Compare commits

..

6 Commits

Author SHA1 Message Date
Bruce MacDonald
f84cc9939c allow ollama.com to call inference and info endpoints
- By default allow ollama.com to call inference and info endpoints this can be overridden by setting an OLLAMA_HOSTS env var
2024-08-12 12:44:50 -07:00
royjhan
5b3a21b578 add metrics to docs (#6079) 2024-08-07 14:43:44 -07:00
Kyle Kelley
ad0c19dde4 Use llama3.1 in tools example (#5985)
* Use llama3.1 in tools example

* Update api.md
2024-08-07 17:20:50 -04:00
Jesse Gross
69eb06c40e Merge pull request #6145 from ollama/jessegross/bug5840
Fix crash on startup when trying to clean up unused files (#5840)
2024-08-07 11:24:15 -07:00
Jesse Gross
1829fb61bd manifest: Fix crash on startup when trying to clean up unused files (#5840)
Currently if the config field is missing in the manifest file (or
corrupted), Ollama will crash when it tries to read it. This can
happen at startup or when pulling new models.

This data is mostly just used for showing model information so we
can be tolerant of it not being present - it is not required to
run the models. Besides avoiding crashing, this also gives us the
ability to restructure the config in the future by pulling it
into the main manifest file.
2024-08-07 10:30:44 -07:00
Jesse Gross
685a53534b manifest: Don't prune layers if we can't open a manifest file
If there is an error when opening a manifest file (corrupted, permission denied, etc.)
then the referenced layers will not be included in the list of active
layers. This causes them to be deleted when pruning happens at startup
or a model is pulled.

In such a situation, we should prefer to preserve data in the hopes that
it can be recovered rather than being agressive about deletion.
2024-08-06 23:11:19 -07:00
6 changed files with 132 additions and 76 deletions

View File

@@ -669,7 +669,7 @@ curl http://localhost:11434/api/chat -d '{
```
curl http://localhost:11434/api/chat -d '{
"model": "mistral",
"model": "llama3.1",
"messages": [
{
"role": "user",
@@ -708,7 +708,7 @@ curl http://localhost:11434/api/chat -d '{
```json
{
"model": "mistral:7b-instruct-v0.3-q4_K_M",
"model": "llama3.1",
"created_at": "2024-07-22T20:33:28.123648Z",
"message": {
"role": "assistant",
@@ -1175,7 +1175,10 @@ curl http://localhost:11434/api/embed -d '{
"embeddings": [[
0.010071029, -0.0017594862, 0.05007221, 0.04692972, 0.054916814,
0.008599704, 0.105441414, -0.025878139, 0.12958129, 0.031952348
]]
]],
"total_duration": 14143917,
"load_duration": 1019500,
"prompt_eval_count": 8
}
```

View File

@@ -57,6 +57,11 @@ func Host() *url.URL {
}
}
// HasCustomOrigins returns true if custom origins are configured. Origins can be configured via the OLLAMA_ORIGINS environment variable.
func HasCustomOrigins() bool {
return Var("OLLAMA_ORIGINS") != ""
}
// Origins returns a list of allowed origins. Origins can be configured via the OLLAMA_ORIGINS environment variable.
func Origins() (origins []string) {
if s := Var("OLLAMA_ORIGINS"); s != "" {

View File

@@ -250,19 +250,21 @@ func GetModel(name string) (*Model, error) {
Template: template.DefaultTemplate,
}
filename, err := GetBlobsPath(manifest.Config.Digest)
if err != nil {
return nil, err
}
if manifest.Config.Digest != "" {
filename, err := GetBlobsPath(manifest.Config.Digest)
if err != nil {
return nil, err
}
configFile, err := os.Open(filename)
if err != nil {
return nil, err
}
defer configFile.Close()
configFile, err := os.Open(filename)
if err != nil {
return nil, err
}
defer configFile.Close()
if err := json.NewDecoder(configFile).Decode(&model.Config); err != nil {
return nil, err
if err := json.NewDecoder(configFile).Decode(&model.Config); err != nil {
return nil, err
}
}
for _, layer := range manifest.Layers {
@@ -714,8 +716,7 @@ func deleteUnusedLayers(skipModelPath *ModelPath, deleteMap map[string]struct{})
// save (i.e. delete from the deleteMap) any files used in other manifests
manifest, _, err := GetManifest(fmp)
if err != nil {
//nolint:nilerr
return nil
return err
}
for _, layer := range manifest.Layers {
@@ -782,7 +783,8 @@ func PruneLayers() error {
err = deleteUnusedLayers(nil, deleteMap)
if err != nil {
return err
slog.Error(fmt.Sprintf("couldn't remove unused layers: %v", err))
return nil
}
slog.Info(fmt.Sprintf("total unused blobs removed: %d", len(deleteMap)))
@@ -839,7 +841,9 @@ func PushModel(ctx context.Context, name string, regOpts *registryOptions, fn fu
var layers []*Layer
layers = append(layers, manifest.Layers...)
layers = append(layers, manifest.Config)
if manifest.Config.Digest != "" {
layers = append(layers, &manifest.Config)
}
for _, layer := range layers {
if err := uploadBlob(ctx, mp, layer, regOpts, fn); err != nil {
@@ -890,7 +894,9 @@ func PullModel(ctx context.Context, name string, regOpts *registryOptions, fn fu
for _, l := range manifest.Layers {
deleteMap[l.Digest] = struct{}{}
}
deleteMap[manifest.Config.Digest] = struct{}{}
if manifest.Config.Digest != "" {
deleteMap[manifest.Config.Digest] = struct{}{}
}
}
}
@@ -907,7 +913,9 @@ func PullModel(ctx context.Context, name string, regOpts *registryOptions, fn fu
var layers []*Layer
layers = append(layers, manifest.Layers...)
layers = append(layers, manifest.Config)
if manifest.Config.Digest != "" {
layers = append(layers, &manifest.Config)
}
skipVerify := make(map[string]bool)
for _, layer := range layers {
@@ -971,7 +979,8 @@ func PullModel(ctx context.Context, name string, regOpts *registryOptions, fn fu
fn(api.ProgressResponse{Status: "removing any unused layers"})
err = deleteUnusedLayers(nil, deleteMap)
if err != nil {
return err
slog.Error(fmt.Sprintf("couldn't remove unused layers: %v", err))
fn(api.ProgressResponse{Status: fmt.Sprintf("couldn't remove unused layers: %v", err)})
}
}

View File

@@ -2,6 +2,7 @@ package server
import (
"crypto/sha256"
"errors"
"fmt"
"io"
"os"
@@ -61,6 +62,10 @@ func NewLayer(r io.Reader, mediatype string) (*Layer, error) {
}
func NewLayerFromLayer(digest, mediatype, from string) (*Layer, error) {
if digest == "" {
return nil, errors.New("creating new layer from layer with empty digest")
}
blob, err := GetBlobsPath(digest)
if err != nil {
return nil, err
@@ -81,6 +86,10 @@ func NewLayerFromLayer(digest, mediatype, from string) (*Layer, error) {
}
func (l *Layer) Open() (io.ReadSeekCloser, error) {
if l.Digest == "" {
return nil, errors.New("opening layer with empty digest")
}
blob, err := GetBlobsPath(l.Digest)
if err != nil {
return nil, err
@@ -90,13 +99,17 @@ func (l *Layer) Open() (io.ReadSeekCloser, error) {
}
func (l *Layer) Remove() error {
if l.Digest == "" {
return nil
}
ms, err := Manifests()
if err != nil {
return err
}
for _, m := range ms {
for _, layer := range append(m.Layers, m.Config) {
for _, layer := range append(m.Layers, &m.Config) {
if layer.Digest == l.Digest {
// something is using this layer
return nil

View File

@@ -16,7 +16,7 @@ import (
type Manifest struct {
SchemaVersion int `json:"schemaVersion"`
MediaType string `json:"mediaType"`
Config *Layer `json:"config"`
Config Layer `json:"config"`
Layers []*Layer `json:"layers"`
filepath string
@@ -25,7 +25,7 @@ type Manifest struct {
}
func (m *Manifest) Size() (size int64) {
for _, layer := range append(m.Layers, m.Config) {
for _, layer := range append(m.Layers, &m.Config) {
size += layer.Size
}
@@ -46,11 +46,13 @@ func (m *Manifest) Remove() error {
}
func (m *Manifest) RemoveLayers() error {
for _, layer := range append(m.Layers, m.Config) {
if err := layer.Remove(); errors.Is(err, os.ErrNotExist) {
slog.Debug("layer does not exist", "digest", layer.Digest)
} else if err != nil {
return err
for _, layer := range append(m.Layers, &m.Config) {
if layer.Digest != "" {
if err := layer.Remove(); errors.Is(err, os.ErrNotExist) {
slog.Debug("layer does not exist", "digest", layer.Digest)
} else if err != nil {
return err
}
}
}
@@ -113,7 +115,7 @@ func WriteManifest(name model.Name, config *Layer, layers []*Layer) error {
m := Manifest{
SchemaVersion: 2,
MediaType: "application/vnd.docker.distribution.manifest.v2+json",
Config: config,
Config: *config,
Layers: layers,
}

View File

@@ -824,17 +824,20 @@ func (s *Server) ListModelsHandler(c *gin.Context) {
models := []api.ListModelResponse{}
for n, m := range ms {
f, err := m.Config.Open()
if err != nil {
slog.Warn("bad manifest filepath", "name", n, "error", err)
continue
}
defer f.Close()
var cf ConfigV2
if err := json.NewDecoder(f).Decode(&cf); err != nil {
slog.Warn("bad manifest config", "name", n, "error", err)
continue
if m.Config.Digest != "" {
f, err := m.Config.Open()
if err != nil {
slog.Warn("bad manifest filepath", "name", n, "error", err)
continue
}
defer f.Close()
if err := json.NewDecoder(f).Decode(&cf); err != nil {
slog.Warn("bad manifest config", "name", n, "error", err)
continue
}
}
// tag should never be masked
@@ -1048,52 +1051,73 @@ func allowedHostsMiddleware(addr net.Addr) gin.HandlerFunc {
}
func (s *Server) GenerateRoutes() http.Handler {
config := cors.DefaultConfig()
config.AllowWildcard = true
config.AllowBrowserExtensions = true
config.AllowHeaders = []string{"Authorization", "Content-Type", "User-Agent", "Accept", "X-Requested-With"}
baseConfig := cors.DefaultConfig()
baseConfig.AllowWildcard = true
baseConfig.AllowBrowserExtensions = true
baseConfig.AllowHeaders = []string{"Authorization", "Content-Type", "User-Agent", "Accept", "X-Requested-With"}
openAIProperties := []string{"lang", "package-version", "os", "arch", "runtime", "runtime-version", "async"}
for _, prop := range openAIProperties {
config.AllowHeaders = append(config.AllowHeaders, "x-stainless-"+prop)
baseConfig.AllowHeaders = append(baseConfig.AllowHeaders, "x-stainless-"+prop)
}
config.AllowOrigins = envconfig.Origins()
r := gin.Default()
r.Use(
cors.New(config),
allowedHostsMiddleware(s.addr),
)
r.POST("/api/pull", s.PullModelHandler)
r.POST("/api/generate", s.GenerateHandler)
r.POST("/api/chat", s.ChatHandler)
r.POST("/api/embed", s.EmbedHandler)
r.POST("/api/embeddings", s.EmbeddingsHandler)
r.POST("/api/create", s.CreateModelHandler)
r.POST("/api/push", s.PushModelHandler)
r.POST("/api/copy", s.CopyModelHandler)
r.DELETE("/api/delete", s.DeleteModelHandler)
r.POST("/api/show", s.ShowModelHandler)
r.POST("/api/blobs/:digest", s.CreateBlobHandler)
r.HEAD("/api/blobs/:digest", s.HeadBlobHandler)
r.GET("/api/ps", s.ProcessHandler)
openConfig := baseConfig
openConfig.AllowOrigins = envconfig.Origins()
if !envconfig.HasCustomOrigins() {
openConfig.AllowOrigins = append(openConfig.AllowOrigins, "https://ollama.com")
openConfig.AllowOrigins = append(openConfig.AllowOrigins, "https://www.ollama.com")
}
// Compatibility endpoints
r.POST("/v1/chat/completions", openai.ChatMiddleware(), s.ChatHandler)
r.POST("/v1/completions", openai.CompletionsMiddleware(), s.GenerateHandler)
r.POST("/v1/embeddings", openai.EmbeddingsMiddleware(), s.EmbedHandler)
r.GET("/v1/models", openai.ListMiddleware(), s.ListModelsHandler)
r.GET("/v1/models/:model", openai.RetrieveMiddleware(), s.ShowModelHandler)
openBaseGroup := r.Group("/")
openBaseGroup.Use(cors.New(openConfig), allowedHostsMiddleware(s.addr))
{
openBaseGroup.GET("/", func(c *gin.Context) { c.String(http.StatusOK, "Ollama is running") })
openBaseGroup.HEAD("/", func(c *gin.Context) { c.String(http.StatusOK, "Ollama is running") })
}
for _, method := range []string{http.MethodGet, http.MethodHead} {
r.Handle(method, "/", func(c *gin.Context) {
c.String(http.StatusOK, "Ollama is running")
openAPIGroup := r.Group("/api")
openAPIGroup.Use(cors.New(openConfig), allowedHostsMiddleware(s.addr))
{
openAPIGroup.OPTIONS("/*path", func(c *gin.Context) {
c.Status(http.StatusOK)
})
openAPIGroup.POST("/pull", s.PullModelHandler)
openAPIGroup.POST("/generate", s.GenerateHandler)
openAPIGroup.POST("/chat", s.ChatHandler)
openAPIGroup.POST("/embed", s.EmbedHandler)
openAPIGroup.POST("/embeddings", s.EmbeddingsHandler)
openAPIGroup.POST("/show", s.ShowModelHandler)
openAPIGroup.GET("/tags", s.ListModelsHandler)
openAPIGroup.HEAD("/tags", s.ListModelsHandler)
openAPIGroup.GET("/version", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"version": version.Version}) })
openAPIGroup.HEAD("/version", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"version": version.Version}) })
}
r.Handle(method, "/api/tags", s.ListModelsHandler)
r.Handle(method, "/api/version", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"version": version.Version})
})
restrictedConfig := baseConfig
restrictedConfig.AllowOrigins = envconfig.Origins()
restrictedAPIGroup := r.Group("/api")
restrictedAPIGroup.Use(cors.New(restrictedConfig), allowedHostsMiddleware(s.addr))
{
restrictedAPIGroup.POST("/create", s.CreateModelHandler)
restrictedAPIGroup.POST("/push", s.PushModelHandler)
restrictedAPIGroup.POST("/copy", s.CopyModelHandler)
restrictedAPIGroup.DELETE("/delete", s.DeleteModelHandler)
restrictedAPIGroup.POST("/blobs/:digest", s.CreateBlobHandler)
restrictedAPIGroup.HEAD("/blobs/:digest", s.HeadBlobHandler)
restrictedAPIGroup.GET("/ps", s.ProcessHandler)
}
openAIConfig := baseConfig
openAIConfig.AllowOrigins = envconfig.Origins()
openAIGroup := r.Group("/v1")
openAIGroup.Use(cors.New(openAIConfig), allowedHostsMiddleware(s.addr))
{
openAIGroup.POST("/chat/completions", openai.ChatMiddleware(), s.ChatHandler)
openAIGroup.POST("/completions", openai.CompletionsMiddleware(), s.GenerateHandler)
openAIGroup.POST("/embeddings", openai.EmbeddingsMiddleware(), s.EmbedHandler)
openAIGroup.GET("/models", openai.ListMiddleware(), s.ListModelsHandler)
openAIGroup.GET("/models/:model", openai.RetrieveMiddleware(), s.ShowModelHandler)
}
return r