mirror of
https://github.com/ollama/ollama.git
synced 2026-04-18 08:54:13 +02:00
Compare commits
2 Commits
pdevine/sa
...
parth/fix-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c73feaf73d | ||
|
|
268c2a1df1 |
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package server
|
package manifest
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
95
manifest/paths.go
Normal file
95
manifest/paths.go
Normal 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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
205
server/images.go
205
server/images.go
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(), ®istryOptions{}, fn); err != nil {
|
if err := PullModel(ctx, name.String(), ®istryOptions{}, 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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"net/url"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
@@ -35,22 +36,25 @@ func Unqualified(n Name) error {
|
|||||||
const MissingPart = "!MISSING!"
|
const MissingPart = "!MISSING!"
|
||||||
|
|
||||||
const (
|
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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,10 +91,11 @@ func (k partKind) String() string {
|
|||||||
// It is not guaranteed to be valid. Use [Name.IsValid] to check if the name
|
// It is not guaranteed to be valid. Use [Name.IsValid] to check if the name
|
||||||
// is valid.
|
// is valid.
|
||||||
type Name struct {
|
type Name struct {
|
||||||
Host string
|
Host string
|
||||||
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:
|
||||||
|
|||||||
@@ -32,10 +32,11 @@ func TestParseNameParts(t *testing.T) {
|
|||||||
{
|
{
|
||||||
in: "scheme://host:port/namespace/model:tag",
|
in: "scheme://host:port/namespace/model:tag",
|
||||||
want: Name{
|
want: Name{
|
||||||
Host: "host:port",
|
Host: "host:port",
|
||||||
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"),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
Layers: layers,
|
MediaType: "application/vnd.docker.distribution.manifest.v2+json",
|
||||||
},
|
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)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user