modelpath.go 3.81 KB
Newer Older
Patrick Devine's avatar
Patrick Devine committed
1
2
3
package server

import (
4
	"errors"
Patrick Devine's avatar
Patrick Devine committed
5
	"fmt"
Michael Yang's avatar
Michael Yang committed
6
	"net/url"
Patrick Devine's avatar
Patrick Devine committed
7
8
	"os"
	"path/filepath"
Michael Yang's avatar
Michael Yang committed
9
	"runtime"
Patrick Devine's avatar
Patrick Devine committed
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
	"strings"
)

type ModelPath struct {
	ProtocolScheme string
	Registry       string
	Namespace      string
	Repository     string
	Tag            string
}

const (
	DefaultRegistry       = "registry.ollama.ai"
	DefaultNamespace      = "library"
	DefaultTag            = "latest"
	DefaultProtocolScheme = "https"
)

28
29
30
31
32
33
var (
	ErrInvalidImageFormat = errors.New("invalid image format")
	ErrInvalidProtocol    = errors.New("invalid protocol scheme")
	ErrInsecureProtocol   = errors.New("insecure protocol http")
)

34
func ParseModelPath(name string) ModelPath {
35
36
37
38
39
40
41
	mp := ModelPath{
		ProtocolScheme: DefaultProtocolScheme,
		Registry:       DefaultRegistry,
		Namespace:      DefaultNamespace,
		Repository:     "",
		Tag:            DefaultTag,
	}
Patrick Devine's avatar
Patrick Devine committed
42

Michael Yang's avatar
Michael Yang committed
43
44
45
46
	before, after, found := strings.Cut(name, "://")
	if found {
		mp.ProtocolScheme = before
		name = after
47
48
	}

49
	name = strings.ReplaceAll(name, string(os.PathSeparator), "/")
50
	parts := strings.Split(name, "/")
51
	switch len(parts) {
Patrick Devine's avatar
Patrick Devine committed
52
	case 3:
53
54
55
		mp.Registry = parts[0]
		mp.Namespace = parts[1]
		mp.Repository = parts[2]
Patrick Devine's avatar
Patrick Devine committed
56
	case 2:
57
58
		mp.Namespace = parts[0]
		mp.Repository = parts[1]
Patrick Devine's avatar
Patrick Devine committed
59
	case 1:
60
		mp.Repository = parts[0]
Patrick Devine's avatar
Patrick Devine committed
61
62
	}

63
	if repo, tag, found := strings.Cut(mp.Repository, ":"); found {
64
65
		mp.Repository = repo
		mp.Tag = tag
Patrick Devine's avatar
Patrick Devine committed
66
67
	}

68
	return mp
Patrick Devine's avatar
Patrick Devine committed
69
70
}

71
72
73
74
75
76
77
78
79
80
81
82
83
84
var errModelPathInvalid = errors.New("invalid model path")

func (mp ModelPath) Validate() error {
	if mp.Repository == "" {
		return fmt.Errorf("%w: model repository name is required", errModelPathInvalid)
	}

	if strings.Contains(mp.Tag, ":") {
		return fmt.Errorf("%w: ':' (colon) is not allowed in tag names", errModelPathInvalid)
	}

	return nil
}

Patrick Devine's avatar
Patrick Devine committed
85
86
87
88
89
90
91
92
93
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 {
94
95
96
97
98
	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)
Patrick Devine's avatar
Patrick Devine committed
99
	}
100
	return fmt.Sprintf("%s/%s/%s:%s", mp.Registry, mp.Namespace, mp.Repository, mp.Tag)
Patrick Devine's avatar
Patrick Devine committed
101
102
}

103
104
105
106
107
108
// modelsDir returns the value of the OLLAMA_MODELS environment variable or the user's home directory if OLLAMA_MODELS is not set.
// The models directory is where Ollama stores its model files and manifests.
func modelsDir() (string, error) {
	if models, exists := os.LookupEnv("OLLAMA_MODELS"); exists {
		return models, nil
	}
Patrick Devine's avatar
Patrick Devine committed
109
110
111
112
	home, err := os.UserHomeDir()
	if err != nil {
		return "", err
	}
113
114
	return filepath.Join(home, ".ollama", "models"), nil
}
Patrick Devine's avatar
Patrick Devine committed
115

116
117
118
119
120
// 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) {
	dir, err := modelsDir()
	if err != nil {
		return "", err
Patrick Devine's avatar
Patrick Devine committed
121
122
	}

123
	return filepath.Join(dir, "manifests", mp.Registry, mp.Namespace, mp.Repository, mp.Tag), nil
Patrick Devine's avatar
Patrick Devine committed
124
125
}

Michael Yang's avatar
Michael Yang committed
126
127
128
129
130
131
132
func (mp ModelPath) BaseURL() *url.URL {
	return &url.URL{
		Scheme: mp.ProtocolScheme,
		Host:   mp.Registry,
	}
}

Patrick Devine's avatar
Patrick Devine committed
133
func GetManifestPath() (string, error) {
134
	dir, err := modelsDir()
Patrick Devine's avatar
Patrick Devine committed
135
136
137
138
	if err != nil {
		return "", err
	}

139
	path := filepath.Join(dir, "manifests")
Michael Yang's avatar
Michael Yang committed
140
	if err := os.MkdirAll(path, 0o755); err != nil {
Michael Yang's avatar
Michael Yang committed
141
142
143
144
		return "", err
	}

	return path, nil
Patrick Devine's avatar
Patrick Devine committed
145
146
}

Patrick Devine's avatar
Patrick Devine committed
147
func GetBlobsPath(digest string) (string, error) {
148
	dir, err := modelsDir()
Patrick Devine's avatar
Patrick Devine committed
149
150
151
152
	if err != nil {
		return "", err
	}

Michael Yang's avatar
Michael Yang committed
153
154
155
156
	if runtime.GOOS == "windows" {
		digest = strings.ReplaceAll(digest, ":", "-")
	}

157
	path := filepath.Join(dir, "blobs", digest)
158
159
160
161
162
163
	dirPath := filepath.Dir(path)
	if digest == "" {
		dirPath = path
	}

	if err := os.MkdirAll(dirPath, 0o755); err != nil {
Patrick Devine's avatar
Patrick Devine committed
164
165
166
		return "", err
	}

Michael Yang's avatar
Michael Yang committed
167
	return path, nil
Patrick Devine's avatar
Patrick Devine committed
168
}