Compare commits

...

2 Commits

Author SHA1 Message Date
Patrick Devine
c73feaf73d Clean up the manifest and modelpath (#13807) 2026-01-21 19:08:21 -08:00
sunyongyue
268c2a1df1 fix: remove multiline option in non-experimental mode
Since commit #13694, it has been behaving strangely.
2026-01-22 02:02:54 +08:00
21 changed files with 414 additions and 661 deletions

View File

@@ -21,14 +21,6 @@ import (
"github.com/ollama/ollama/types/model" "github.com/ollama/ollama/types/model"
) )
type MultilineState int
const (
MultilineNone MultilineState = iota
MultilinePrompt
MultilineSystem
)
func generateInteractive(cmd *cobra.Command, opts runOptions) error { func generateInteractive(cmd *cobra.Command, opts runOptions) error {
usage := func() { usage := func() {
fmt.Fprintln(os.Stderr, "Available Commands:") fmt.Fprintln(os.Stderr, "Available Commands:")
@@ -130,7 +122,6 @@ func generateInteractive(cmd *cobra.Command, opts runOptions) error {
defer fmt.Printf(readline.EndBracketedPaste) defer fmt.Printf(readline.EndBracketedPaste)
var sb strings.Builder var sb strings.Builder
var multiline MultilineState
var thinkExplicitlySet bool = opts.Think != nil var thinkExplicitlySet bool = opts.Think != nil
for { for {
@@ -153,35 +144,6 @@ func generateInteractive(cmd *cobra.Command, opts runOptions) error {
} }
switch { switch {
case multiline != MultilineNone:
// check if there's a multiline terminating string
before, ok := strings.CutSuffix(line, `"""`)
sb.WriteString(before)
if !ok {
fmt.Fprintln(&sb)
continue
}
switch multiline {
case MultilineSystem:
opts.System = sb.String()
opts.Messages = append(opts.Messages, api.Message{Role: "system", Content: opts.System})
fmt.Println("Set system message.")
sb.Reset()
}
multiline = MultilineNone
scanner.Prompt.UseAlt = false
case strings.HasPrefix(line, `"""`):
line := strings.TrimPrefix(line, `"""`)
line, ok := strings.CutSuffix(line, `"""`)
sb.WriteString(line)
if !ok {
// no multiline terminating string; need more input
fmt.Fprintln(&sb)
multiline = MultilinePrompt
scanner.Prompt.UseAlt = true
}
case scanner.Pasting: case scanner.Pasting:
fmt.Fprintln(&sb, line) fmt.Fprintln(&sb, line)
continue continue
@@ -334,41 +296,19 @@ func generateInteractive(cmd *cobra.Command, opts runOptions) error {
opts.Options[args[2]] = fp[args[2]] opts.Options[args[2]] = fp[args[2]]
case "system": case "system":
if len(args) < 3 { if len(args) < 3 {
usageSet() fmt.Println("Usage: /set system <message>")
continue continue
} }
multiline = MultilineSystem opts.System = strings.Join(args[2:], " ")
newMessage := api.Message{Role: "system", Content: opts.System}
line := strings.Join(args[2:], " ")
line, ok := strings.CutPrefix(line, `"""`)
if !ok {
multiline = MultilineNone
} else {
// only cut suffix if the line is multiline
line, ok = strings.CutSuffix(line, `"""`)
if ok {
multiline = MultilineNone
}
}
sb.WriteString(line)
if multiline != MultilineNone {
scanner.Prompt.UseAlt = true
continue
}
opts.System = sb.String() // for display in modelfile
newMessage := api.Message{Role: "system", Content: sb.String()}
// Check if the slice is not empty and the last message is from 'system' // Check if the slice is not empty and the last message is from 'system'
if len(opts.Messages) > 0 && opts.Messages[len(opts.Messages)-1].Role == "system" { if len(opts.Messages) > 0 && opts.Messages[len(opts.Messages)-1].Role == "system" {
// Replace the last message
opts.Messages[len(opts.Messages)-1] = newMessage opts.Messages[len(opts.Messages)-1] = newMessage
} else { } else {
opts.Messages = append(opts.Messages, newMessage) opts.Messages = append(opts.Messages, newMessage)
} }
fmt.Println("Set system message.") fmt.Println("Set system message.")
sb.Reset()
continue continue
default: default:
fmt.Printf("Unknown command '/set %s'. Type /? for help\n", args[1]) fmt.Printf("Unknown command '/set %s'. Type /? for help\n", args[1])
@@ -483,7 +423,7 @@ func generateInteractive(cmd *cobra.Command, opts runOptions) error {
sb.WriteString(line) sb.WriteString(line)
} }
if sb.Len() > 0 && multiline == MultilineNone { if sb.Len() > 0 {
newMessage := api.Message{Role: "user", Content: sb.String()} newMessage := api.Message{Role: "user", Content: sb.String()}
if opts.MultiModal { if opts.MultiModal {

View File

@@ -1,4 +1,4 @@
package server package manifest
import ( import (
"crypto/sha256" "crypto/sha256"
@@ -14,7 +14,7 @@ type Layer struct {
Size int64 `json:"size"` Size int64 `json:"size"`
From string `json:"from,omitempty"` From string `json:"from,omitempty"`
Name string `json:"name,omitempty"` // tensor name, e.g., "text_encoder/model.embed_tokens.weight" Name string `json:"name,omitempty"` // tensor name, e.g., "text_encoder/model.embed_tokens.weight"
status string Status string `json:"-"`
} }
const ( const (
@@ -22,7 +22,7 @@ const (
) )
func NewLayer(r io.Reader, mediatype string) (Layer, error) { func NewLayer(r io.Reader, mediatype string) (Layer, error) {
blobs, err := GetBlobsPath("") blobs, err := BlobsPath("")
if err != nil { if err != nil {
return Layer{}, err return Layer{}, err
} }
@@ -45,7 +45,7 @@ func NewLayer(r io.Reader, mediatype string) (Layer, error) {
} }
digest := fmt.Sprintf("sha256:%x", sha256sum.Sum(nil)) digest := fmt.Sprintf("sha256:%x", sha256sum.Sum(nil))
blob, err := GetBlobsPath(digest) blob, err := BlobsPath(digest)
if err != nil { if err != nil {
return Layer{}, err return Layer{}, err
} }
@@ -65,7 +65,7 @@ func NewLayer(r io.Reader, mediatype string) (Layer, error) {
MediaType: mediatype, MediaType: mediatype,
Digest: digest, Digest: digest,
Size: n, Size: n,
status: fmt.Sprintf("%s %s", status, digest), Status: fmt.Sprintf("%s %s", status, digest),
}, nil }, 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") return Layer{}, errors.New("creating new layer from layer with empty digest")
} }
blob, err := GetBlobsPath(digest) blob, err := BlobsPath(digest)
if err != nil { if err != nil {
return Layer{}, err return Layer{}, err
} }
@@ -89,7 +89,7 @@ func NewLayerFromLayer(digest, mediatype, from string) (Layer, error) {
Digest: digest, Digest: digest,
Size: fi.Size(), Size: fi.Size(),
From: from, From: from,
status: fmt.Sprintf("using existing layer %s", digest), Status: fmt.Sprintf("using existing layer %s", digest),
}, nil }, nil
} }
@@ -98,7 +98,7 @@ func (l *Layer) Open() (io.ReadSeekCloser, error) {
return nil, errors.New("opening layer with empty digest") return nil, errors.New("opening layer with empty digest")
} }
blob, err := GetBlobsPath(l.Digest) blob, err := BlobsPath(l.Digest)
if err != nil { if err != nil {
return nil, err 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 { if err != nil {
return err return err
} }

View File

@@ -1,10 +1,9 @@
package server package manifest
import ( import (
"crypto/sha256" "crypto/sha256"
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io" "io"
"log/slog" "log/slog"
@@ -33,12 +32,38 @@ func (m *Manifest) Size() (size int64) {
return 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 { func (m *Manifest) Remove() error {
if err := os.Remove(m.filepath); err != nil { if err := os.Remove(m.filepath); err != nil {
return err return err
} }
manifests, err := GetManifestPath() manifests, err := Path()
if err != nil { if err != nil {
return err return err
} }
@@ -70,11 +95,11 @@ func (m *Manifest) RemoveLayers() error {
if _, used := inUse[layer.Digest]; used { if _, used := inUse[layer.Digest]; used {
continue continue
} }
blob, err := GetBlobsPath(layer.Digest) blob, err := BlobsPath(layer.Digest)
if err != nil { if err != nil {
return err 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) slog.Debug("layer does not exist", "digest", layer.Digest)
} else if err != nil { } else if err != nil {
return err return err
@@ -89,7 +114,7 @@ func ParseNamedManifest(n model.Name) (*Manifest, error) {
return nil, model.Unqualified(n) return nil, model.Unqualified(n)
} }
manifests, err := GetManifestPath() manifests, err := Path()
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -121,7 +146,7 @@ func ParseNamedManifest(n model.Name) (*Manifest, error) {
} }
func WriteManifest(name model.Name, config Layer, layers []Layer) error { func WriteManifest(name model.Name, config Layer, layers []Layer) error {
manifests, err := GetManifestPath() manifests, err := Path()
if err != nil { if err != nil {
return err 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) { func Manifests(continueOnError bool) (map[model.Name]*Manifest, error) {
manifests, err := GetManifestPath() manifests, err := Path()
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -1,4 +1,4 @@
package server package manifest
import ( import (
"encoding/json" "encoding/json"

95
manifest/paths.go Normal file
View File

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

View File

@@ -28,6 +28,7 @@ import (
"github.com/ollama/ollama/format" "github.com/ollama/ollama/format"
ofs "github.com/ollama/ollama/fs" ofs "github.com/ollama/ollama/fs"
"github.com/ollama/ollama/fs/ggml" "github.com/ollama/ollama/fs/ggml"
"github.com/ollama/ollama/manifest"
"github.com/ollama/ollama/template" "github.com/ollama/ollama/template"
"github.com/ollama/ollama/types/errtypes" "github.com/ollama/ollama/types/errtypes"
"github.com/ollama/ollama/types/model" "github.com/ollama/ollama/types/model"
@@ -90,7 +91,7 @@ func (s *Server) CreateHandler(c *gin.Context) {
ch <- resp ch <- resp
} }
oldManifest, _ := ParseNamedManifest(name) oldManifest, _ := manifest.ParseNamedManifest(name)
var baseLayers []*layerGGML var baseLayers []*layerGGML
var err error var err error
@@ -123,9 +124,9 @@ func (s *Server) CreateHandler(c *gin.Context) {
} }
if err == nil && !remote && (config.Renderer == "" || config.Parser == "" || config.Requires == "") { if err == nil && !remote && (config.Renderer == "" || config.Parser == "" || config.Requires == "") {
manifest, mErr := ParseNamedManifest(fromName) mf, mErr := manifest.ParseNamedManifest(fromName)
if mErr == nil && manifest.Config.Digest != "" { if mErr == nil && mf.Config.Digest != "" {
configPath, pErr := GetBlobsPath(manifest.Config.Digest) configPath, pErr := manifest.BlobsPath(mf.Config.Digest)
if pErr == nil { if pErr == nil {
if cfgFile, fErr := os.Open(configPath); fErr == nil { if cfgFile, fErr := os.Open(configPath); fErr == nil {
var baseConfig model.ConfigV2 var baseConfig model.ConfigV2
@@ -342,7 +343,7 @@ func detectModelTypeFromFiles(files map[string]string) string {
return "gguf" return "gguf"
} else { } else {
// try to see if we can find a gguf file even without the file extension // 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 { if err != nil {
slog.Error("error getting blobs path", "file", fn) slog.Error("error getting blobs path", "file", fn)
return "" return ""
@@ -394,7 +395,7 @@ func convertFromSafetensors(files map[string]string, baseLayers []*layerGGML, is
return nil, fmt.Errorf("%w: %s: %s", errFilePath, err, fp) return nil, fmt.Errorf("%w: %s: %s", errFilePath, err, fp)
} }
blobPath, err := GetBlobsPath(digest) blobPath, err := manifest.BlobsPath(digest)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -432,7 +433,7 @@ func convertFromSafetensors(files map[string]string, baseLayers []*layerGGML, is
return nil, err return nil, err
} }
layer, err := NewLayer(t, mediaType) layer, err := manifest.NewLayer(t, mediaType)
if err != nil { if err != nil {
return nil, err 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) { 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 { for _, layer := range baseLayers {
if layer.GGML != nil { if layer.GGML != nil {
quantType := strings.ToUpper(cmp.Or(r.Quantize, r.Quantization)) 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 { for _, layer := range layers {
if layer.status != "" { if layer.Status != "" {
fn(api.ProgressResponse{Status: layer.status}) fn(api.ProgressResponse{Status: layer.Status})
} }
} }
fn(api.ProgressResponse{Status: "writing manifest"}) 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 return err
} }
@@ -577,7 +578,7 @@ func quantizeLayer(layer *layerGGML, quantizeType string, fn func(resp api.Progr
return nil, err return nil, err
} }
blob, err := GetBlobsPath(layer.Digest) blob, err := manifest.BlobsPath(layer.Digest)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -599,7 +600,7 @@ func quantizeLayer(layer *layerGGML, quantizeType string, fn func(resp api.Progr
} }
temp.Seek(0, io.SeekStart) temp.Seek(0, io.SeekStart)
fn(api.ProgressResponse{Status: "verifying conversion"}) fn(api.ProgressResponse{Status: "verifying conversion"})
newLayer, err := NewLayer(temp, layer.MediaType) newLayer, err := manifest.NewLayer(temp, layer.MediaType)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -619,7 +620,7 @@ func ggufLayers(digest string, fn func(resp api.ProgressResponse)) ([]*layerGGML
var layers []*layerGGML var layers []*layerGGML
fn(api.ProgressResponse{Status: "parsing GGUF"}) fn(api.ProgressResponse{Status: "parsing GGUF"})
blobPath, err := GetBlobsPath(digest) blobPath, err := manifest.BlobsPath(digest)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -654,7 +655,7 @@ func ggufLayers(digest string, fn func(resp api.ProgressResponse)) ([]*layerGGML
mediatype = "application/vnd.ollama.image.projector" mediatype = "application/vnd.ollama.image.projector"
} }
layer, err := NewLayerFromLayer(digest, mediatype, blob.Name()) layer, err := manifest.NewLayerFromLayer(digest, mediatype, blob.Name())
if err != nil { if err != nil {
slog.Debug("could not create new layer from layer", "error", err) slog.Debug("could not create new layer from layer", "error", err)
return nil, err return nil, err
@@ -665,8 +666,8 @@ func ggufLayers(digest string, fn func(resp api.ProgressResponse)) ([]*layerGGML
return detectChatTemplate(layers) return detectChatTemplate(layers)
} }
func removeLayer(layers []Layer, mediatype string) []Layer { func removeLayer(layers []manifest.Layer, mediatype string) []manifest.Layer {
return slices.DeleteFunc(layers, func(layer Layer) bool { return slices.DeleteFunc(layers, func(layer manifest.Layer) bool {
if layer.MediaType != mediatype { if layer.MediaType != mediatype {
return false 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") layers = removeLayer(layers, "application/vnd.ollama.image.template")
if _, err := template.Parse(t); err != nil { if _, err := template.Parse(t); err != nil {
return nil, fmt.Errorf("%w: %s", errBadTemplate, err) return nil, fmt.Errorf("%w: %s", errBadTemplate, err)
@@ -690,7 +691,7 @@ func setTemplate(layers []Layer, t string) ([]Layer, error) {
} }
blob := strings.NewReader(t) 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 { if err != nil {
return nil, err return nil, err
} }
@@ -699,11 +700,11 @@ func setTemplate(layers []Layer, t string) ([]Layer, error) {
return layers, nil 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") layers = removeLayer(layers, "application/vnd.ollama.image.system")
if s != "" { if s != "" {
blob := strings.NewReader(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 { if err != nil {
return nil, err return nil, err
} }
@@ -712,9 +713,9 @@ func setSystem(layers []Layer, s string) ([]Layer, error) {
return layers, nil 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) 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 { if err != nil {
return nil, err return nil, err
} }
@@ -722,7 +723,7 @@ func setLicense(layers []Layer, l string) ([]Layer, error) {
return layers, nil 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 { if p == nil {
p = make(map[string]any) p = make(map[string]any)
} }
@@ -731,7 +732,7 @@ func setParameters(layers []Layer, p map[string]any) ([]Layer, error) {
continue continue
} }
digestPath, err := GetBlobsPath(layer.Digest) digestPath, err := manifest.BlobsPath(layer.Digest)
if err != nil { if err != nil {
return nil, err 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 { if err := json.NewEncoder(&b).Encode(p); err != nil {
return nil, err 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 { if err != nil {
return nil, err return nil, err
} }
@@ -773,7 +774,7 @@ func setParameters(layers []Layer, p map[string]any) ([]Layer, error) {
return layers, nil 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 // this leaves the old messages intact if no new messages were specified
// which may not be the correct behaviour // which may not be the correct behaviour
if len(m) == 0 { 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 { if err := json.NewEncoder(&b).Encode(m); err != nil {
return nil, err 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 { if err != nil {
return nil, err return nil, err
} }
@@ -794,7 +795,7 @@ func setMessages(layers []Layer, m []api.Message) ([]Layer, error) {
return layers, nil 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)) digests := make([]string, len(layers))
for i, layer := range layers { for i, layer := range layers {
digests[i] = layer.Digest 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 { if err := json.NewEncoder(&b).Encode(config); err != nil {
return nil, err 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 { if err != nil {
return nil, err return nil, err
} }

View File

@@ -10,6 +10,7 @@ import (
"testing" "testing"
"github.com/ollama/ollama/api" "github.com/ollama/ollama/api"
"github.com/ollama/ollama/manifest"
) )
func TestConvertFromSafetensors(t *testing.T) { 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 // Helper function to create a new layer and return its digest
makeTemp := func(content string) string { 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 { if err != nil {
t.Fatalf("Failed to create layer: %v", err) t.Fatalf("Failed to create layer: %v", err)
} }

View File

@@ -24,6 +24,8 @@ import (
"github.com/ollama/ollama/api" "github.com/ollama/ollama/api"
"github.com/ollama/ollama/format" "github.com/ollama/ollama/format"
"github.com/ollama/ollama/manifest"
"github.com/ollama/ollama/types/model"
) )
const maxRetries = 6 const maxRetries = 6
@@ -456,7 +458,7 @@ func (b *blobDownload) Wait(ctx context.Context, fn func(api.ProgressResponse))
} }
type downloadOpts struct { type downloadOpts struct {
mp ModelPath n model.Name
digest string digest string
regOpts *registryOptions regOpts *registryOptions
fn func(api.ProgressResponse) 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 // downloadBlob downloads a blob from the registry and stores it in the blobs directory
func downloadBlob(ctx context.Context, opts downloadOpts) (cacheHit bool, _ error) { func downloadBlob(ctx context.Context, opts downloadOpts) (cacheHit bool, _ error) {
if opts.digest == "" { 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 { if err != nil {
return false, err 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}) data, ok := blobDownloadManager.LoadOrStore(opts.digest, &blobDownload{Name: fp, Digest: opts.digest})
download := data.(*blobDownload) download := data.(*blobDownload)
if !ok { if !ok {
requestURL := opts.mp.BaseURL() requestURL := opts.n.BaseURL()
requestURL = requestURL.JoinPath("v2", opts.mp.GetNamespaceRepository(), "blobs", opts.digest) requestURL = requestURL.JoinPath("v2", opts.n.DisplayNamespaceModel(), "blobs", opts.digest)
if err := download.Prepare(ctx, requestURL, opts.regOpts); err != nil { if err := download.Prepare(ctx, requestURL, opts.regOpts); err != nil {
blobDownloadManager.Delete(opts.digest) blobDownloadManager.Delete(opts.digest)
return false, err return false, err

View File

@@ -4,7 +4,6 @@ import (
"bytes" "bytes"
"context" "context"
"crypto/sha256" "crypto/sha256"
"encoding/hex"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
@@ -24,6 +23,7 @@ import (
"github.com/ollama/ollama/api" "github.com/ollama/ollama/api"
"github.com/ollama/ollama/envconfig" "github.com/ollama/ollama/envconfig"
"github.com/ollama/ollama/fs/gguf" "github.com/ollama/ollama/fs/gguf"
"github.com/ollama/ollama/manifest"
"github.com/ollama/ollama/model/parsers" "github.com/ollama/ollama/model/parsers"
"github.com/ollama/ollama/parser" "github.com/ollama/ollama/parser"
"github.com/ollama/ollama/template" "github.com/ollama/ollama/template"
@@ -274,44 +274,22 @@ func (m *Model) String() string {
return modelfile.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) { func GetModel(name string) (*Model, error) {
mp := ParseModelPath(name) n := model.ParseName(name)
manifest, digest, err := GetManifest(mp) mf, err := manifest.ParseNamedManifest(n)
if err != nil { if err != nil {
return nil, err return nil, err
} }
model := &Model{ m := &Model{
Name: mp.GetFullTagname(), Name: n.String(),
ShortName: mp.GetShortTagname(), ShortName: n.DisplayShortest(),
Digest: digest, Digest: mf.Digest(),
Template: template.DefaultTemplate, Template: template.DefaultTemplate,
} }
if manifest.Config.Digest != "" { if mf.Config.Digest != "" {
filename, err := GetBlobsPath(manifest.Config.Digest) filename, err := manifest.BlobsPath(mf.Config.Digest)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -322,29 +300,29 @@ func GetModel(name string) (*Model, error) {
} }
defer configFile.Close() 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 return nil, err
} }
} }
for _, layer := range manifest.Layers { for _, layer := range mf.Layers {
filename, err := GetBlobsPath(layer.Digest) filename, err := manifest.BlobsPath(layer.Digest)
if err != nil { if err != nil {
return nil, err return nil, err
} }
switch layer.MediaType { switch layer.MediaType {
case "application/vnd.ollama.image.model": case "application/vnd.ollama.image.model":
model.ModelPath = filename m.ModelPath = filename
model.ParentModel = layer.From m.ParentModel = layer.From
case "application/vnd.ollama.image.embed": case "application/vnd.ollama.image.embed":
// Deprecated in versions > 0.1.2 // Deprecated in versions > 0.1.2
// TODO: remove this warning in a future version // 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.") slog.Info("WARNING: model contains embeddings, but embeddings in modelfiles have been deprecated and will be ignored.")
case "application/vnd.ollama.image.adapter": case "application/vnd.ollama.image.adapter":
model.AdapterPaths = append(model.AdapterPaths, filename) m.AdapterPaths = append(m.AdapterPaths, filename)
case "application/vnd.ollama.image.projector": case "application/vnd.ollama.image.projector":
model.ProjectorPaths = append(model.ProjectorPaths, filename) m.ProjectorPaths = append(m.ProjectorPaths, filename)
case "application/vnd.ollama.image.prompt", case "application/vnd.ollama.image.prompt",
"application/vnd.ollama.image.template": "application/vnd.ollama.image.template":
bts, err := os.ReadFile(filename) bts, err := os.ReadFile(filename)
@@ -352,7 +330,7 @@ func GetModel(name string) (*Model, error) {
return nil, err return nil, err
} }
model.Template, err = template.Parse(string(bts)) m.Template, err = template.Parse(string(bts))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -362,7 +340,7 @@ func GetModel(name string) (*Model, error) {
return nil, err return nil, err
} }
model.System = string(bts) m.System = string(bts)
case "application/vnd.ollama.image.params": case "application/vnd.ollama.image.params":
params, err := os.Open(filename) params, err := os.Open(filename)
if err != nil { if err != nil {
@@ -371,7 +349,7 @@ func GetModel(name string) (*Model, error) {
defer params.Close() defer params.Close()
// parse model options parameters into a map so that we can see which fields have been specified explicitly // 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 return nil, err
} }
case "application/vnd.ollama.image.messages": case "application/vnd.ollama.image.messages":
@@ -381,7 +359,7 @@ func GetModel(name string) (*Model, error) {
} }
defer msgs.Close() 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 return nil, err
} }
case "application/vnd.ollama.image.license": case "application/vnd.ollama.image.license":
@@ -389,11 +367,11 @@ func GetModel(name string) (*Model, error) {
if err != nil { if err != nil {
return nil, err 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 { func CopyModel(src, dst model.Name) error {
@@ -408,7 +386,7 @@ func CopyModel(src, dst model.Name) error {
return nil return nil
} }
manifests, err := GetManifestPath() manifests, err := manifest.Path()
if err != nil { if err != nil {
return err return err
} }
@@ -437,7 +415,7 @@ func CopyModel(src, dst model.Name) error {
func deleteUnusedLayers(deleteMap map[string]struct{}) error { func deleteUnusedLayers(deleteMap map[string]struct{}) error {
// Ignore corrupt manifests to avoid blocking deletion of layers that are freshly orphaned // 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 { if err != nil {
return err return err
} }
@@ -452,7 +430,7 @@ func deleteUnusedLayers(deleteMap map[string]struct{}) error {
// only delete the files which are still in the deleteMap // only delete the files which are still in the deleteMap
for k := range deleteMap { for k := range deleteMap {
fp, err := GetBlobsPath(k) fp, err := manifest.BlobsPath(k)
if err != nil { if err != nil {
slog.Info(fmt.Sprintf("couldn't get file path for '%s': %v", k, err)) slog.Info(fmt.Sprintf("couldn't get file path for '%s': %v", k, err))
continue continue
@@ -468,7 +446,7 @@ func deleteUnusedLayers(deleteMap map[string]struct{}) error {
func PruneLayers() error { func PruneLayers() error {
deleteMap := make(map[string]struct{}) deleteMap := make(map[string]struct{})
p, err := GetBlobsPath("") p, err := manifest.BlobsPath("")
if err != nil { if err != nil {
return err return err
} }
@@ -483,9 +461,9 @@ func PruneLayers() error {
name := blob.Name() name := blob.Name()
name = strings.ReplaceAll(name, "-", ":") name = strings.ReplaceAll(name, "-", ":")
_, err := GetBlobsPath(name) _, err := manifest.BlobsPath(name)
if err != nil { if err != nil {
if errors.Is(err, ErrInvalidDigestFormat) { if errors.Is(err, manifest.ErrInvalidDigestFormat) {
// remove invalid blobs (e.g. partial downloads) // remove invalid blobs (e.g. partial downloads)
if err := os.Remove(filepath.Join(p, blob.Name())); err != nil { if err := os.Remove(filepath.Join(p, blob.Name())); err != nil {
slog.Error("couldn't remove blob", "blob", blob.Name(), "error", err) slog.Error("couldn't remove blob", "blob", blob.Name(), "error", err)
@@ -510,63 +488,30 @@ func PruneLayers() error {
return nil 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 { 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"}) fn(api.ProgressResponse{Status: "retrieving manifest"})
if mp.ProtocolScheme == "http" && !regOpts.Insecure { if n.ProtocolScheme == "http" && !regOpts.Insecure {
return errInsecureProtocol return errInsecureProtocol
} }
manifest, _, err := GetManifest(mp) mf, err := manifest.ParseNamedManifest(n)
if err != nil { if err != nil {
fn(api.ProgressResponse{Status: "couldn't retrieve manifest"}) fn(api.ProgressResponse{Status: "couldn't retrieve manifest"})
return err return err
} }
var layers []Layer var layers []manifest.Layer
layers = append(layers, manifest.Layers...) layers = append(layers, mf.Layers...)
if manifest.Config.Digest != "" { if mf.Config.Digest != "" {
layers = append(layers, manifest.Config) layers = append(layers, mf.Config)
} }
// Use fast transfer for models with tensor layers (many small blobs) // Use fast transfer for models with tensor layers (many small blobs)
if hasTensorLayers(layers) { if hasTensorLayers(layers) {
// Read raw manifest JSON to preserve tensor metadata fields // Read raw manifest JSON to preserve tensor metadata fields
manifestPath, err := mp.GetManifestPath() manifestPath, err := manifest.PathForName(n)
if err != nil { if err != nil {
return err return err
} }
@@ -574,7 +519,7 @@ func PushModel(ctx context.Context, name string, regOpts *registryOptions, fn fu
if err != nil { if err != nil {
return err 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 return err
} }
fn(api.ProgressResponse{Status: "success"}) fn(api.ProgressResponse{Status: "success"})
@@ -582,17 +527,17 @@ func PushModel(ctx context.Context, name string, regOpts *registryOptions, fn fu
} }
for _, layer := range layers { 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)) slog.Info(fmt.Sprintf("error uploading blob: %v", err))
return err return err
} }
} }
fn(api.ProgressResponse{Status: "pushing manifest"}) fn(api.ProgressResponse{Status: "pushing manifest"})
requestURL := mp.BaseURL() requestURL := n.BaseURL()
requestURL = requestURL.JoinPath("v2", mp.GetNamespaceRepository(), "manifests", mp.Tag) requestURL = requestURL.JoinPath("v2", n.DisplayNamespaceModel(), "manifests", n.Tag)
manifestJSON, err := json.Marshal(manifest) manifestJSON, err := json.Marshal(mf)
if err != nil { if err != nil {
return err 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 { 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 // build deleteMap to prune unused layers
deleteMap := make(map[string]struct{}) deleteMap := make(map[string]struct{})
manifest, _, err := GetManifest(mp) existingMf, err := manifest.ParseNamedManifest(n)
if errors.Is(err, os.ErrNotExist) { if errors.Is(err, os.ErrNotExist) {
// noop // noop
} else if err != nil { } else if err != nil {
slog.Warn("pulling model with bad existing manifest", "name", name, "error", err) slog.Warn("pulling model with bad existing manifest", "name", name, "error", err)
} else { } else {
for _, l := range manifest.Layers { for _, l := range existingMf.Layers {
deleteMap[l.Digest] = struct{}{} deleteMap[l.Digest] = struct{}{}
} }
if manifest.Config.Digest != "" { if existingMf.Config.Digest != "" {
deleteMap[manifest.Config.Digest] = struct{}{} deleteMap[existingMf.Config.Digest] = struct{}{}
} }
} }
if mp.ProtocolScheme == "http" && !regOpts.Insecure { if n.ProtocolScheme == "http" && !regOpts.Insecure {
return errInsecureProtocol return errInsecureProtocol
} }
fn(api.ProgressResponse{Status: "pulling manifest"}) fn(api.ProgressResponse{Status: "pulling manifest"})
manifest, err = pullModelManifest(ctx, mp, regOpts) mf, err := pullModelManifest(ctx, n, regOpts)
if err != nil { if err != nil {
return fmt.Errorf("pull model manifest: %s", err) return fmt.Errorf("pull model manifest: %s", err)
} }
var layers []Layer var layers []manifest.Layer
layers = append(layers, manifest.Layers...) layers = append(layers, mf.Layers...)
if manifest.Config.Digest != "" { if mf.Config.Digest != "" {
layers = append(layers, manifest.Config) layers = append(layers, mf.Config)
} }
// Use fast transfer for models with tensor layers (many small blobs) // Use fast transfer for models with tensor layers (many small blobs)
if hasTensorLayers(layers) { 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 return err
} }
fn(api.ProgressResponse{Status: "success"}) 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) skipVerify := make(map[string]bool)
for _, layer := range layers { for _, layer := range layers {
cacheHit, err := downloadBlob(ctx, downloadOpts{ cacheHit, err := downloadBlob(ctx, downloadOpts{
mp: mp, n: n,
digest: layer.Digest, digest: layer.Digest,
regOpts: regOpts, regOpts: regOpts,
fn: fn, 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 err := verifyBlob(layer.Digest); err != nil {
if errors.Is(err, errDigestMismatch) { if errors.Is(err, errDigestMismatch) {
fp, err := GetBlobsPath(layer.Digest) fp, err := manifest.BlobsPath(layer.Digest)
if err != nil { if err != nil {
return err return err
} }
@@ -692,16 +637,16 @@ func PullModel(ctx context.Context, name string, regOpts *registryOptions, fn fu
for _, layer := range layers { for _, layer := range layers {
delete(deleteMap, layer.Digest) delete(deleteMap, layer.Digest)
} }
delete(deleteMap, manifest.Config.Digest) delete(deleteMap, mf.Config.Digest)
fn(api.ProgressResponse{Status: "writing manifest"}) fn(api.ProgressResponse{Status: "writing manifest"})
manifestJSON, err := json.Marshal(manifest) manifestJSON, err := json.Marshal(mf)
if err != nil { if err != nil {
return err return err
} }
fp, err := mp.GetManifestPath() fp, err := manifest.PathForName(n)
if err != nil { if err != nil {
return err 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. // hasTensorLayers checks if any layer has tensor media type.
func hasTensorLayers(layers []Layer) bool { func hasTensorLayers(layers []manifest.Layer) bool {
for _, layer := range layers { for _, layer := range layers {
if layer.MediaType == MediaTypeImageTensor { if layer.MediaType == manifest.MediaTypeImageTensor {
return true return true
} }
} }
@@ -738,7 +683,7 @@ func hasTensorLayers(layers []Layer) bool {
} }
// pullWithTransfer uses the simplified x/transfer package for downloading blobs. // 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)) blobs := make([]transfer.Blob, len(layers))
for i, layer := range layers { for i, layer := range layers {
blobs[i] = transfer.Blob{ 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 { if err != nil {
return err return err
} }
base := mp.BaseURL() base := n.BaseURL()
if base.Scheme != "http" && regOpts != nil && regOpts.Insecure { if base.Scheme != "http" && regOpts != nil && regOpts.Insecure {
base.Scheme = "http" base.Scheme = "http"
} }
@@ -784,7 +729,7 @@ func pullWithTransfer(ctx context.Context, mp ModelPath, layers []Layer, manifes
Blobs: blobs, Blobs: blobs,
BaseURL: baseURL, BaseURL: baseURL,
DestDir: destDir, DestDir: destDir,
Repository: mp.GetNamespaceRepository(), Repository: n.DisplayNamespaceModel(),
Progress: progress, Progress: progress,
Token: regOpts.Token, Token: regOpts.Token,
GetToken: getToken, GetToken: getToken,
@@ -795,12 +740,12 @@ func pullWithTransfer(ctx context.Context, mp ModelPath, layers []Layer, manifes
// Write manifest // Write manifest
fn(api.ProgressResponse{Status: "writing manifest"}) fn(api.ProgressResponse{Status: "writing manifest"})
manifestJSON, err := json.Marshal(manifest) manifestJSON, err := json.Marshal(mf)
if err != nil { if err != nil {
return err return err
} }
fp, err := mp.GetManifestPath() fp, err := manifest.PathForName(n)
if err != nil { if err != nil {
return err 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. // 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)) blobs := make([]transfer.Blob, len(layers))
for i, layer := range layers { for i, layer := range layers {
blobs[i] = transfer.Blob{ 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 { if err != nil {
return err return err
} }
base := mp.BaseURL() base := n.BaseURL()
if base.Scheme != "http" && regOpts != nil && regOpts.Insecure { if base.Scheme != "http" && regOpts != nil && regOpts.Insecure {
base.Scheme = "http" base.Scheme = "http"
} }
@@ -864,13 +809,13 @@ func pushWithTransfer(ctx context.Context, mp ModelPath, layers []Layer, manifes
GetToken: getToken, GetToken: getToken,
Logger: slog.Default(), Logger: slog.Default(),
Manifest: manifestJSON, Manifest: manifestJSON,
ManifestRef: mp.Tag, ManifestRef: n.Tag,
Repository: mp.GetNamespaceRepository(), Repository: n.DisplayNamespaceModel(),
}) })
} }
func pullModelManifest(ctx context.Context, mp ModelPath, regOpts *registryOptions) (*Manifest, error) { func pullModelManifest(ctx context.Context, n model.Name, regOpts *registryOptions) (*manifest.Manifest, error) {
requestURL := mp.BaseURL().JoinPath("v2", mp.GetNamespaceRepository(), "manifests", mp.Tag) requestURL := n.BaseURL().JoinPath("v2", n.DisplayNamespaceModel(), "manifests", n.Tag)
headers := make(http.Header) headers := make(http.Header)
headers.Set("Accept", "application/vnd.docker.distribution.manifest.v2+json") 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() defer resp.Body.Close()
var m Manifest var m manifest.Manifest
if err := json.NewDecoder(resp.Body).Decode(&m); err != nil { if err := json.NewDecoder(resp.Body).Decode(&m); err != nil {
return nil, err return nil, err
} }
@@ -1042,7 +987,7 @@ func parseRegistryChallenge(authStr string) registryChallenge {
var errDigestMismatch = errors.New("digest mismatch, file must be downloaded again") var errDigestMismatch = errors.New("digest mismatch, file must be downloaded again")
func verifyBlob(digest string) error { func verifyBlob(digest string) error {
fp, err := GetBlobsPath(digest) fp, err := manifest.BlobsPath(digest)
if err != nil { if err != nil {
return err return err
} }

View File

@@ -13,6 +13,7 @@ import (
"github.com/ollama/ollama/api" "github.com/ollama/ollama/api"
"github.com/ollama/ollama/fs/ggml" "github.com/ollama/ollama/fs/ggml"
"github.com/ollama/ollama/manifest"
"github.com/ollama/ollama/template" "github.com/ollama/ollama/template"
"github.com/ollama/ollama/types/model" "github.com/ollama/ollama/types/model"
) )
@@ -20,19 +21,19 @@ import (
var intermediateBlobs map[string]string = make(map[string]string) var intermediateBlobs map[string]string = make(map[string]string)
type layerGGML struct { type layerGGML struct {
Layer manifest.Layer
*ggml.GGML *ggml.GGML
} }
func parseFromModel(ctx context.Context, name model.Name, fn func(api.ProgressResponse)) (layers []*layerGGML, err error) { 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 { switch {
case errors.Is(err, os.ErrNotExist): case errors.Is(err, os.ErrNotExist):
if err := PullModel(ctx, name.String(), &registryOptions{}, fn); err != nil { if err := PullModel(ctx, name.String(), &registryOptions{}, fn); err != nil {
return nil, err return nil, err
} }
m, err = ParseNamedManifest(name) m, err = manifest.ParseNamedManifest(name)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -41,7 +42,7 @@ func parseFromModel(ctx context.Context, name model.Name, fn func(api.ProgressRe
} }
for _, layer := range m.Layers { 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 { if err != nil {
return nil, err 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", case "application/vnd.ollama.image.model",
"application/vnd.ollama.image.projector", "application/vnd.ollama.image.projector",
"application/vnd.ollama.image.adapter": "application/vnd.ollama.image.adapter":
blobpath, err := GetBlobsPath(layer.Digest) blobpath, err := manifest.BlobsPath(layer.Digest)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -81,12 +82,12 @@ func detectChatTemplate(layers []*layerGGML) ([]*layerGGML, error) {
if t, err := template.Named(s); err != nil { if t, err := template.Named(s); err != nil {
slog.Debug("template detection", "error", err, "template", s) slog.Debug("template detection", "error", err, "template", s)
} else { } 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 { if err != nil {
return nil, err 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}) layers = append(layers, &layerGGML{layer, nil})
if t.Parameters != nil { if t.Parameters != nil {
@@ -95,7 +96,7 @@ func detectChatTemplate(layers []*layerGGML) ([]*layerGGML, error) {
return nil, err 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 { if err != nil {
return nil, err return nil, err
} }

View File

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

View File

@@ -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)
}
})
}
}

View File

@@ -39,6 +39,7 @@ import (
"github.com/ollama/ollama/fs/ggml" "github.com/ollama/ollama/fs/ggml"
"github.com/ollama/ollama/llm" "github.com/ollama/ollama/llm"
"github.com/ollama/ollama/logutil" "github.com/ollama/ollama/logutil"
"github.com/ollama/ollama/manifest"
"github.com/ollama/ollama/middleware" "github.com/ollama/ollama/middleware"
"github.com/ollama/ollama/model/parsers" "github.com/ollama/ollama/model/parsers"
"github.com/ollama/ollama/model/renderers" "github.com/ollama/ollama/model/renderers"
@@ -974,7 +975,7 @@ func (s *Server) PushHandler(c *gin.Context) {
// is. // is.
func getExistingName(n model.Name) (model.Name, error) { func getExistingName(n model.Name) (model.Name, error) {
var zero model.Name var zero model.Name
existing, err := Manifests(true) existing, err := manifest.Manifests(true)
if err != nil { if err != nil {
return zero, err return zero, err
} }
@@ -1018,7 +1019,7 @@ func (s *Server) DeleteHandler(c *gin.Context) {
return return
} }
m, err := ParseNamedManifest(n) m, err := manifest.ParseNamedManifest(n)
if err != nil { if err != nil {
switch { switch {
case os.IsNotExist(err): case os.IsNotExist(err):
@@ -1080,7 +1081,7 @@ func (s *Server) ShowHandler(c *gin.Context) {
func GetModelInfo(req api.ShowRequest) (*api.ShowResponse, error) { func GetModelInfo(req api.ShowRequest) (*api.ShowResponse, error) {
name := model.ParseName(req.Model) name := model.ParseName(req.Model)
if !name.IsValid() { if !name.IsValid() {
return nil, ErrModelPathInvalid return nil, model.Unqualified(name)
} }
name, err := getExistingName(name) name, err := getExistingName(name)
if err != nil { 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 // For safetensors LLM models (experimental), populate details from config.json
if m.Config.ModelFormat == "safetensors" && slices.Contains(m.Config.Capabilities, "completion") { 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 != "" { if arch, ok := info["general.architecture"].(string); ok && arch != "" {
modelDetails.Family = 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 // 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 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} msgs[i] = api.Message{Role: msg.Role, Content: msg.Content}
} }
manifest, err := ParseNamedManifest(name) mf, err := manifest.ParseNamedManifest(name)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -1147,7 +1148,7 @@ func GetModelInfo(req api.ShowRequest) (*api.ShowResponse, error) {
Details: modelDetails, Details: modelDetails,
Messages: msgs, Messages: msgs,
Capabilities: m.Capabilities(), Capabilities: m.Capabilities(),
ModifiedAt: manifest.fi.ModTime(), ModifiedAt: mf.FileInfo().ModTime(),
Requires: m.Config.Requires, Requires: m.Config.Requires,
// Several integrations crash on a nil/omitempty+empty ModelInfo, so by // Several integrations crash on a nil/omitempty+empty ModelInfo, so by
// default we return an empty map. // 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) { if slices.Contains(m.Capabilities(), model.CapabilityImage) {
// Populate tensor info if verbose // Populate tensor info if verbose
if req.Verbose { if req.Verbose {
if tensors, err := xserver.GetSafetensorsTensorInfo(name.String()); err == nil { if tensors, err := xserver.GetSafetensorsTensorInfo(name); err == nil {
resp.Tensors = tensors 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 // For safetensors LLM models (experimental), populate ModelInfo from config.json
if m.Config.ModelFormat == "safetensors" && slices.Contains(m.Config.Capabilities, "completion") { 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 resp.ModelInfo = info
} }
// Populate tensor info if verbose // Populate tensor info if verbose
if req.Verbose { if req.Verbose {
if tensors, err := xserver.GetSafetensorsTensorInfo(name.String()); err == nil { if tensors, err := xserver.GetSafetensorsTensorInfo(name); err == nil {
resp.Tensors = tensors 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) { func (s *Server) ListHandler(c *gin.Context) {
ms, err := Manifests(true) ms, err := manifest.Manifests(true)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return return
@@ -1316,8 +1317,8 @@ func (s *Server) ListHandler(c *gin.Context) {
RemoteModel: cf.RemoteModel, RemoteModel: cf.RemoteModel,
RemoteHost: cf.RemoteHost, RemoteHost: cf.RemoteHost,
Size: m.Size(), Size: m.Size(),
Digest: m.digest, Digest: m.Digest(),
ModifiedAt: m.fi.ModTime(), ModifiedAt: m.FileInfo().ModTime(),
Details: api.ModelDetails{ Details: api.ModelDetails{
Format: cf.ModelFormat, Format: cf.ModelFormat,
Family: cf.ModelFamily, Family: cf.ModelFamily,
@@ -1376,7 +1377,7 @@ func (s *Server) CopyHandler(c *gin.Context) {
} }
func (s *Server) HeadBlobHandler(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 { if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()}) c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return return
@@ -1392,7 +1393,7 @@ func (s *Server) HeadBlobHandler(c *gin.Context) {
func (s *Server) CreateBlobHandler(c *gin.Context) { func (s *Server) CreateBlobHandler(c *gin.Context) {
if ib, ok := intermediateBlobs[c.Param("digest")]; ok { if ib, ok := intermediateBlobs[c.Param("digest")]; ok {
p, err := GetBlobsPath(ib) p, err := manifest.BlobsPath(ib)
if err != nil { if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return 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 { if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()}) c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return return
@@ -1428,7 +1429,7 @@ func (s *Server) CreateBlobHandler(c *gin.Context) {
return return
} }
layer, err := NewLayer(c.Request.Body, "") layer, err := manifest.NewLayer(c.Request.Body, "")
if err != nil { if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return return
@@ -1628,7 +1629,7 @@ func Serve(ln net.Listener) error {
slog.SetDefault(logutil.NewLogger(os.Stderr, envconfig.LogLevel())) slog.SetDefault(logutil.NewLogger(os.Stderr, envconfig.LogLevel()))
slog.Info("server config", "env", envconfig.Values()) slog.Info("server config", "env", envconfig.Values())
blobsDir, err := GetBlobsPath("") blobsDir, err := manifest.BlobsPath("")
if err != nil { if err != nil {
return err return err
} }
@@ -1637,7 +1638,7 @@ func Serve(ln net.Listener) error {
} }
if !envconfig.NoPrune() { 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) slog.Warn("corrupt manifests detected, skipping prune operation. Re-pull or delete to clear", "error", err)
} else { } else {
// clean up unused layers and manifests // clean up unused layers and manifests
@@ -1645,12 +1646,12 @@ func Serve(ln net.Listener) error {
return err return err
} }
manifestsPath, err := GetManifestPath() manifestsPath, err := manifest.Path()
if err != nil { if err != nil {
return err return err
} }
if err := PruneDirectory(manifestsPath); err != nil { if err := manifest.PruneDirectory(manifestsPath); err != nil {
return err return err
} }
} }

View File

@@ -25,6 +25,7 @@ import (
"github.com/ollama/ollama/convert" "github.com/ollama/ollama/convert"
"github.com/ollama/ollama/envconfig" "github.com/ollama/ollama/envconfig"
"github.com/ollama/ollama/fs/ggml" "github.com/ollama/ollama/fs/ggml"
"github.com/ollama/ollama/manifest"
"github.com/ollama/ollama/types/model" "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) 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 { if err != nil {
t.Fatalf("parse manifest: %v", err) t.Fatalf("parse manifest: %v", err)
} }
if manifest.Config.Digest == "" { if mf.Config.Digest == "" {
t.Fatalf("unexpected empty config digest for child manifest") 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 { if err != nil {
t.Fatalf("config blob path: %v", err) t.Fatalf("config blob path: %v", err)
} }

View File

@@ -10,6 +10,7 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/ollama/ollama/api" "github.com/ollama/ollama/api"
"github.com/ollama/ollama/manifest"
"github.com/ollama/ollama/types/model" "github.com/ollama/ollama/types/model"
) )
@@ -93,13 +94,13 @@ func TestDeleteDuplicateLayers(t *testing.T) {
t.Fatal(err) 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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
// create a manifest with duplicate layers // 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) t.Fatal(err)
} }

View File

@@ -21,12 +21,14 @@ import (
"github.com/ollama/ollama/api" "github.com/ollama/ollama/api"
"github.com/ollama/ollama/format" "github.com/ollama/ollama/format"
"github.com/ollama/ollama/manifest"
"github.com/ollama/ollama/types/model"
) )
var blobUploadManager sync.Map var blobUploadManager sync.Map
type blobUpload struct { type blobUpload struct {
Layer manifest.Layer
Total int64 Total int64
Completed atomic.Int64 Completed atomic.Int64
@@ -51,7 +53,7 @@ const (
) )
func (b *blobUpload) Prepare(ctx context.Context, requestURL *url.URL, opts *registryOptions) error { 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 { if err != nil {
return err return err
} }
@@ -59,7 +61,7 @@ func (b *blobUpload) Prepare(ctx context.Context, requestURL *url.URL, opts *reg
if b.From != "" { if b.From != "" {
values := requestURL.Query() values := requestURL.Query()
values.Add("mount", b.Digest) values.Add("mount", b.Digest)
values.Add("from", ParseModelPath(b.From).GetNamespaceRepository()) values.Add("from", model.ParseName(b.From).DisplayNamespaceModel())
requestURL.RawQuery = values.Encode() requestURL.RawQuery = values.Encode()
} }
@@ -128,7 +130,7 @@ func (b *blobUpload) Run(ctx context.Context, opts *registryOptions) {
defer blobUploadManager.Delete(b.Digest) defer blobUploadManager.Delete(b.Digest)
ctx, b.CancelFunc = context.WithCancel(ctx) ctx, b.CancelFunc = context.WithCancel(ctx)
p, err := GetBlobsPath(b.Digest) p, err := manifest.BlobsPath(b.Digest)
if err != nil { if err != nil {
b.err = err b.err = err
return return
@@ -364,9 +366,9 @@ func (p *progressWriter) Rollback() {
p.written = 0 p.written = 0
} }
func uploadBlob(ctx context.Context, mp ModelPath, layer Layer, opts *registryOptions, fn func(api.ProgressResponse)) error { func uploadBlob(ctx context.Context, n model.Name, layer manifest.Layer, opts *registryOptions, fn func(api.ProgressResponse)) error {
requestURL := mp.BaseURL() requestURL := n.BaseURL()
requestURL = requestURL.JoinPath("v2", mp.GetNamespaceRepository(), "blobs", layer.Digest) requestURL = requestURL.JoinPath("v2", n.DisplayNamespaceModel(), "blobs", layer.Digest)
resp, err := makeRequestWithRetry(ctx, http.MethodHead, requestURL, nil, nil, opts) resp, err := makeRequestWithRetry(ctx, http.MethodHead, requestURL, nil, nil, opts)
switch { 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}) data, ok := blobUploadManager.LoadOrStore(layer.Digest, &blobUpload{Layer: layer})
upload := data.(*blobUpload) upload := data.(*blobUpload)
if !ok { if !ok {
requestURL := mp.BaseURL() requestURL := n.BaseURL()
requestURL = requestURL.JoinPath("v2", mp.GetNamespaceRepository(), "blobs/uploads/") requestURL = requestURL.JoinPath("v2", n.DisplayNamespaceModel(), "blobs/uploads/")
if err := upload.Prepare(ctx, requestURL, opts); err != nil { if err := upload.Prepare(ctx, requestURL, opts); err != nil {
blobUploadManager.Delete(layer.Digest) blobUploadManager.Delete(layer.Digest)
return err return err

View File

@@ -7,6 +7,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"log/slog" "log/slog"
"net/url"
"path/filepath" "path/filepath"
"strings" "strings"
) )
@@ -38,19 +39,22 @@ const (
defaultHost = "registry.ollama.ai" defaultHost = "registry.ollama.ai"
defaultNamespace = "library" defaultNamespace = "library"
defaultTag = "latest" defaultTag = "latest"
defaultProtocolScheme = "https"
) )
// DefaultName returns a name with the default values for the host, namespace, // 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 host is ("registry.ollama.ai")
// - The default namespace is ("library") // - The default namespace is ("library")
// - The default tag is ("latest") // - The default tag is ("latest")
// - The default protocol scheme is ("https")
func DefaultName() Name { func DefaultName() Name {
return Name{ return Name{
Host: defaultHost, Host: defaultHost,
Namespace: defaultNamespace, Namespace: defaultNamespace,
Tag: defaultTag, Tag: defaultTag,
ProtocolScheme: defaultProtocolScheme,
} }
} }
@@ -91,6 +95,7 @@ type Name struct {
Namespace string Namespace string
Model string Model string
Tag string Tag string
ProtocolScheme string
} }
// ParseName parses and assembles a Name from a name string. The // 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, "://") scheme, host, ok := strings.Cut(s, "://")
if !ok { if ok {
n.ProtocolScheme = scheme
} else {
host = scheme host = scheme
} }
n.Host = host n.Host = host
@@ -189,12 +196,13 @@ func ParseNameFromFilepath(s string) (n Name) {
return n 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. // preferring the non-empty parts of a.
func Merge(a, b Name) Name { func Merge(a, b Name) Name {
a.Host = cmp.Or(a.Host, b.Host) a.Host = cmp.Or(a.Host, b.Host)
a.Namespace = cmp.Or(a.Namespace, b.Namespace) a.Namespace = cmp.Or(a.Namespace, b.Namespace)
a.Tag = cmp.Or(a.Tag, b.Tag) a.Tag = cmp.Or(a.Tag, b.Tag)
a.ProtocolScheme = cmp.Or(a.ProtocolScheme, b.ProtocolScheme)
return a return a
} }
@@ -305,6 +313,23 @@ func (n Name) EqualFold(o Name) bool {
strings.EqualFold(n.Tag, o.Tag) 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 { func isValidLen(kind partKind, s string) bool {
switch kind { switch kind {
case kindHost: case kindHost:

View File

@@ -36,6 +36,7 @@ func TestParseNameParts(t *testing.T) {
Namespace: "namespace", Namespace: "namespace",
Model: "model", Model: "model",
Tag: "tag", Tag: "tag",
ProtocolScheme: "scheme",
}, },
wantFilepath: filepath.Join("host:port", "namespace", "model", "tag"), wantFilepath: filepath.Join("host:port", "namespace", "model", "tag"),
}, },

View File

@@ -12,8 +12,8 @@ import (
"fmt" "fmt"
"io" "io"
"github.com/ollama/ollama/manifest"
"github.com/ollama/ollama/progress" "github.com/ollama/ollama/progress"
"github.com/ollama/ollama/server"
"github.com/ollama/ollama/types/model" "github.com/ollama/ollama/types/model"
"github.com/ollama/ollama/x/create" "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. // newLayerCreator returns a LayerCreator callback for creating config/JSON layers.
func newLayerCreator() create.LayerCreator { func newLayerCreator() create.LayerCreator {
return func(r io.Reader, mediaType, name string) (create.LayerInfo, error) { 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 { if err != nil {
return create.LayerInfo{}, err return create.LayerInfo{}, err
} }
@@ -141,13 +141,13 @@ func createQuantizedLayers(r io.Reader, name, dtype string, shape []int32, quant
} }
// Create layer for quantized weight // 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 { if err != nil {
return nil, err return nil, err
} }
// Create layer for scales // 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 { if err != nil {
return nil, err 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) // Add qbiases layer if present (affine mode)
if qbiasData != nil { if qbiasData != nil {
qbiasLayer, err := server.NewLayer(bytes.NewReader(qbiasData), server.MediaTypeImageTensor) qbiasLayer, err := manifest.NewLayer(bytes.NewReader(qbiasData), manifest.MediaTypeImageTensor)
if err != nil { if err != nil {
return nil, err 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. // createUnquantizedLayer creates a single tensor layer without quantization.
func createUnquantizedLayer(r io.Reader, name string) ([]create.LayerInfo, error) { 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 { if err != nil {
return nil, err return nil, err
} }
@@ -221,15 +221,15 @@ func newManifestWriter(opts CreateOptions, capabilities []string) create.Manifes
} }
// Create config layer blob // 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 { if err != nil {
return fmt.Errorf("failed to create config layer: %w", err) return fmt.Errorf("failed to create config layer: %w", err)
} }
// Convert LayerInfo to server.Layer // Convert LayerInfo to manifest.Layer
serverLayers := make([]server.Layer, 0, len(layers)) manifestLayers := make([]manifest.Layer, 0, len(layers))
for _, l := range layers { for _, l := range layers {
serverLayers = append(serverLayers, server.Layer{ manifestLayers = append(manifestLayers, manifest.Layer{
MediaType: l.MediaType, MediaType: l.MediaType,
Digest: l.Digest, Digest: l.Digest,
Size: l.Size, Size: l.Size,
@@ -243,19 +243,19 @@ func newManifestWriter(opts CreateOptions, capabilities []string) create.Manifes
if err != nil { if err != nil {
return err 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. // createModelfileLayers creates layers for template, system, and license from Modelfile config.
func createModelfileLayers(mf *ModelfileConfig) ([]server.Layer, error) { func createModelfileLayers(mf *ModelfileConfig) ([]manifest.Layer, error) {
var layers []server.Layer var layers []manifest.Layer
if mf.Template != "" { 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 { if err != nil {
return nil, fmt.Errorf("failed to create template layer: %w", err) 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 != "" { 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 { if err != nil {
return nil, fmt.Errorf("failed to create system layer: %w", err) 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 != "" { 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 { if err != nil {
return nil, fmt.Errorf("failed to create license layer: %w", err) return nil, fmt.Errorf("failed to create license layer: %w", err)
} }

View File

@@ -9,7 +9,8 @@ import (
"strings" "strings"
"github.com/ollama/ollama/api" "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 // modelConfig represents the HuggingFace config.json structure
@@ -35,22 +36,22 @@ type modelConfig struct {
// GetSafetensorsLLMInfo extracts model information from safetensors LLM models. // GetSafetensorsLLMInfo extracts model information from safetensors LLM models.
// It reads the config.json layer and returns a map compatible with GGML's KV format. // It reads the config.json layer and returns a map compatible with GGML's KV format.
func GetSafetensorsLLMInfo(modelName string) (map[string]any, error) { func GetSafetensorsLLMInfo(name model.Name) (map[string]any, error) {
manifest, err := imagegen.LoadManifest(modelName) mf, err := manifest.ParseNamedManifest(name)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to load manifest: %w", err) return nil, fmt.Errorf("failed to load manifest: %w", err)
} }
var config modelConfig 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) return nil, fmt.Errorf("failed to read config.json: %w", err)
} }
// Calculate total tensor bytes from manifest layers // Calculate total tensor bytes from manifest layers
var totalBytes int64 var totalBytes int64
var tensorCount int64 var tensorCount int64
for _, layer := range manifest.Manifest.Layers { for _, layer := range mf.Layers {
if layer.MediaType == "application/vnd.ollama.image.tensor" { if layer.MediaType == manifest.MediaTypeImageTensor {
totalBytes += layer.Size totalBytes += layer.Size
tensorCount++ tensorCount++
} }
@@ -151,27 +152,30 @@ func buildModelInfo(config modelConfig, totalTensorBytes, tensorCount int64) map
// GetSafetensorsTensorInfo extracts tensor information from safetensors model layers. // GetSafetensorsTensorInfo extracts tensor information from safetensors model layers.
// Each tensor is stored as a minimal safetensors file with an 88-byte header containing metadata. // Each tensor is stored as a minimal safetensors file with an 88-byte header containing metadata.
func GetSafetensorsTensorInfo(modelName string) ([]api.Tensor, error) { func GetSafetensorsTensorInfo(name model.Name) ([]api.Tensor, error) {
manifest, err := imagegen.LoadManifest(modelName) mf, err := manifest.ParseNamedManifest(name)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to load manifest: %w", err) return nil, fmt.Errorf("failed to load manifest: %w", err)
} }
return getTensorInfoFromManifest(manifest) return getTensorInfoFromManifest(mf)
} }
// getTensorInfoFromManifest extracts tensor info from a manifest. // getTensorInfoFromManifest extracts tensor info from a manifest.
// This is separated for testability. // 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 var tensors []api.Tensor
for _, layer := range manifest.Manifest.Layers { for _, layer := range mf.Layers {
if layer.MediaType != "application/vnd.ollama.image.tensor" { if layer.MediaType != manifest.MediaTypeImageTensor {
continue continue
} }
// Read the safetensors header from the blob // 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) info, err := readSafetensorsHeader(blobPath)
if err != nil { if err != nil {
// Skip tensors we can't read // 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. // GetSafetensorsDtype returns the quantization type for a safetensors model.
// If the model is quantized (has _scale tensors), returns the quantization type (e.g., "FP8"). // If the model is quantized (has _scale tensors), returns the quantization type (e.g., "FP8").
// Otherwise returns the torch_dtype from config.json. // Otherwise returns the torch_dtype from config.json.
func GetSafetensorsDtype(modelName string) (string, error) { func GetSafetensorsDtype(name model.Name) (string, error) {
manifest, err := imagegen.LoadManifest(modelName) mf, err := manifest.ParseNamedManifest(name)
if err != nil { if err != nil {
return "", fmt.Errorf("failed to load manifest: %w", err) return "", fmt.Errorf("failed to load manifest: %w", err)
} }
// Check if model is quantized by looking for _scale tensors // Check if model is quantized by looking for _scale tensors
for _, layer := range manifest.Manifest.Layers { for _, layer := range mf.Layers {
if layer.MediaType == "application/vnd.ollama.image.tensor" { if layer.MediaType == manifest.MediaTypeImageTensor {
if strings.HasSuffix(layer.Name, "_scale") { if strings.HasSuffix(layer.Name, "_scale") {
// Model is quantized - return FP8 (affine quantization) // Model is quantized - return FP8 (affine quantization)
return "FP8", nil return "FP8", nil
@@ -217,7 +221,7 @@ func GetSafetensorsDtype(modelName string) (string, error) {
var cfg struct { var cfg struct {
TorchDtype string `json:"torch_dtype"` 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) return "", fmt.Errorf("failed to read config.json: %w", err)
} }

View File

@@ -8,7 +8,7 @@ import (
"path/filepath" "path/filepath"
"testing" "testing"
"github.com/ollama/ollama/x/imagegen" "github.com/ollama/ollama/manifest"
) )
func TestBuildModelInfo(t *testing.T) { func TestBuildModelInfo(t *testing.T) {
@@ -451,8 +451,14 @@ func TestParseSafetensorsHeader_Errors(t *testing.T) {
} }
func TestGetTensorInfoFromManifest(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() 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 // Create test tensor blobs
tensors := []struct { tensors := []struct {
@@ -463,26 +469,26 @@ func TestGetTensorInfoFromManifest(t *testing.T) {
}{ }{
{ {
name: "model.embed_tokens.weight", name: "model.embed_tokens.weight",
digest: "sha256:abc123", digest: "sha256:abc123abc123abc123abc123abc123abc123abc123abc123abc123abc123abc0",
dtype: "BF16", dtype: "BF16",
shape: []int64{262144, 2560}, shape: []int64{262144, 2560},
}, },
{ {
name: "model.layers.0.self_attn.q_proj.weight", name: "model.layers.0.self_attn.q_proj.weight",
digest: "sha256:def456", digest: "sha256:def456def456def456def456def456def456def456def456def456def456def0",
dtype: "BF16", dtype: "BF16",
shape: []int64{2560, 2560}, shape: []int64{2560, 2560},
}, },
{ {
name: "model.norm.weight", name: "model.norm.weight",
digest: "sha256:ghi789", digest: "sha256:789789789789789789789789789789789789789789789789789789789789abc0",
dtype: "F32", dtype: "F32",
shape: []int64{2560}, shape: []int64{2560},
}, },
} }
// Create blob files // Create blob files
var layers []imagegen.ManifestLayer var layers []manifest.Layer
for _, tensor := range tensors { for _, tensor := range tensors {
// Create safetensors blob // Create safetensors blob
header := map[string]any{ header := map[string]any{
@@ -498,15 +504,17 @@ func TestGetTensorInfoFromManifest(t *testing.T) {
binary.Write(&buf, binary.LittleEndian, uint64(len(headerJSON))) binary.Write(&buf, binary.LittleEndian, uint64(len(headerJSON)))
buf.Write(headerJSON) buf.Write(headerJSON)
// Write blob file // Write blob file using the digest format expected by GetBlobsPath
blobName := "sha256-" + tensor.digest[7:] blobPath, err := manifest.BlobsPath(tensor.digest)
blobPath := filepath.Join(tempDir, blobName) if err != nil {
t.Fatalf("failed to get blob path: %v", err)
}
if err := os.WriteFile(blobPath, buf.Bytes(), 0o644); err != nil { if err := os.WriteFile(blobPath, buf.Bytes(), 0o644); err != nil {
t.Fatalf("failed to write blob: %v", err) t.Fatalf("failed to write blob: %v", err)
} }
layers = append(layers, imagegen.ManifestLayer{ layers = append(layers, manifest.Layer{
MediaType: "application/vnd.ollama.image.tensor", MediaType: manifest.MediaTypeImageTensor,
Digest: tensor.digest, Digest: tensor.digest,
Size: int64(buf.Len() + 1000), // header + fake data Size: int64(buf.Len() + 1000), // header + fake data
Name: tensor.name, Name: tensor.name,
@@ -514,21 +522,20 @@ func TestGetTensorInfoFromManifest(t *testing.T) {
} }
// Add a non-tensor layer (should be skipped) // Add a non-tensor layer (should be skipped)
layers = append(layers, imagegen.ManifestLayer{ layers = append(layers, manifest.Layer{
MediaType: "application/vnd.ollama.image.json", MediaType: "application/vnd.ollama.image.json",
Digest: "sha256:config", Digest: "sha256:0000000000000000000000000000000000000000000000000000000000000000",
Size: 100, Size: 100,
Name: "config.json", Name: "config.json",
}) })
manifest := &imagegen.ModelManifest{ mf := &manifest.Manifest{
Manifest: &imagegen.Manifest{ SchemaVersion: 2,
MediaType: "application/vnd.docker.distribution.manifest.v2+json",
Layers: layers, Layers: layers,
},
BlobDir: tempDir,
} }
result, err := getTensorInfoFromManifest(manifest) result, err := getTensorInfoFromManifest(mf)
if err != nil { if err != nil {
t.Fatalf("getTensorInfoFromManifest() error = %v", err) t.Fatalf("getTensorInfoFromManifest() error = %v", err)
} }