mirror of
https://github.com/ollama/ollama.git
synced 2025-05-11 02:16:36 +02:00
Update the /api/create endpoint to use JSON (#7935)
Replaces `POST /api/create` to use JSON instead of a Modelfile. This is a breaking change.
This commit is contained in:
parent
459d822b51
commit
86a622cbdc
17 changed files with 1523 additions and 1094 deletions
|
@ -11,18 +11,23 @@ import (
|
|||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/ollama/ollama/api"
|
||||
"github.com/ollama/ollama/envconfig"
|
||||
"github.com/ollama/ollama/llm"
|
||||
)
|
||||
|
||||
var stream bool = false
|
||||
|
||||
func createBinFile(t *testing.T, kv map[string]any, ti []llm.Tensor) string {
|
||||
func createBinFile(t *testing.T, kv map[string]any, ti []llm.Tensor) (string, string) {
|
||||
t.Helper()
|
||||
t.Setenv("OLLAMA_MODELS", cmp.Or(os.Getenv("OLLAMA_MODELS"), t.TempDir()))
|
||||
|
||||
modelDir := envconfig.Models()
|
||||
|
||||
f, err := os.CreateTemp(t.TempDir(), "")
|
||||
if err != nil {
|
||||
|
@ -33,8 +38,21 @@ func createBinFile(t *testing.T, kv map[string]any, ti []llm.Tensor) string {
|
|||
if err := llm.WriteGGUF(f, kv, ti); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Calculate sha256 of file
|
||||
if _, err := f.Seek(0, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
return f.Name()
|
||||
digest, _ := GetSHA256Digest(f)
|
||||
if err := f.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := createLink(f.Name(), filepath.Join(modelDir, "blobs", fmt.Sprintf("sha256-%s", strings.TrimPrefix(digest, "sha256:")))); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
return f.Name(), digest
|
||||
}
|
||||
|
||||
type responseRecorder struct {
|
||||
|
@ -93,13 +111,17 @@ func TestCreateFromBin(t *testing.T) {
|
|||
t.Setenv("OLLAMA_MODELS", p)
|
||||
|
||||
var s Server
|
||||
|
||||
_, digest := createBinFile(t, nil, nil)
|
||||
|
||||
w := createRequest(t, s.CreateHandler, api.CreateRequest{
|
||||
Name: "test",
|
||||
Modelfile: fmt.Sprintf("FROM %s", createBinFile(t, nil, nil)),
|
||||
Stream: &stream,
|
||||
Name: "test",
|
||||
Files: map[string]string{"test.gguf": digest},
|
||||
Stream: &stream,
|
||||
})
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
fmt.Println(w)
|
||||
t.Fatalf("expected status code 200, actual %d", w.Code)
|
||||
}
|
||||
|
||||
|
@ -120,10 +142,12 @@ func TestCreateFromModel(t *testing.T) {
|
|||
t.Setenv("OLLAMA_MODELS", p)
|
||||
var s Server
|
||||
|
||||
_, digest := createBinFile(t, nil, nil)
|
||||
|
||||
w := createRequest(t, s.CreateHandler, api.CreateRequest{
|
||||
Name: "test",
|
||||
Modelfile: fmt.Sprintf("FROM %s", createBinFile(t, nil, nil)),
|
||||
Stream: &stream,
|
||||
Name: "test",
|
||||
Files: map[string]string{"test.gguf": digest},
|
||||
Stream: &stream,
|
||||
})
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
|
@ -135,9 +159,9 @@ func TestCreateFromModel(t *testing.T) {
|
|||
})
|
||||
|
||||
w = createRequest(t, s.CreateHandler, api.CreateRequest{
|
||||
Name: "test2",
|
||||
Modelfile: "FROM test",
|
||||
Stream: &stream,
|
||||
Name: "test2",
|
||||
From: "test",
|
||||
Stream: &stream,
|
||||
})
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
|
@ -162,10 +186,12 @@ func TestCreateRemovesLayers(t *testing.T) {
|
|||
t.Setenv("OLLAMA_MODELS", p)
|
||||
var s Server
|
||||
|
||||
_, digest := createBinFile(t, nil, nil)
|
||||
w := createRequest(t, s.CreateHandler, api.CreateRequest{
|
||||
Name: "test",
|
||||
Modelfile: fmt.Sprintf("FROM %s\nTEMPLATE {{ .Prompt }}", createBinFile(t, nil, nil)),
|
||||
Stream: &stream,
|
||||
Name: "test",
|
||||
Files: map[string]string{"test.gguf": digest},
|
||||
Template: "{{ .Prompt }}",
|
||||
Stream: &stream,
|
||||
})
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
|
@ -183,9 +209,10 @@ func TestCreateRemovesLayers(t *testing.T) {
|
|||
})
|
||||
|
||||
w = createRequest(t, s.CreateHandler, api.CreateRequest{
|
||||
Name: "test",
|
||||
Modelfile: fmt.Sprintf("FROM %s\nTEMPLATE {{ .System }} {{ .Prompt }}", createBinFile(t, nil, nil)),
|
||||
Stream: &stream,
|
||||
Name: "test",
|
||||
Files: map[string]string{"test.gguf": digest},
|
||||
Template: "{{ .System }} {{ .Prompt }}",
|
||||
Stream: &stream,
|
||||
})
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
|
@ -210,10 +237,12 @@ func TestCreateUnsetsSystem(t *testing.T) {
|
|||
t.Setenv("OLLAMA_MODELS", p)
|
||||
var s Server
|
||||
|
||||
_, digest := createBinFile(t, nil, nil)
|
||||
w := createRequest(t, s.CreateHandler, api.CreateRequest{
|
||||
Name: "test",
|
||||
Modelfile: fmt.Sprintf("FROM %s\nSYSTEM Say hi!", createBinFile(t, nil, nil)),
|
||||
Stream: &stream,
|
||||
Name: "test",
|
||||
Files: map[string]string{"test.gguf": digest},
|
||||
System: "Say hi!",
|
||||
Stream: &stream,
|
||||
})
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
|
@ -231,9 +260,10 @@ func TestCreateUnsetsSystem(t *testing.T) {
|
|||
})
|
||||
|
||||
w = createRequest(t, s.CreateHandler, api.CreateRequest{
|
||||
Name: "test",
|
||||
Modelfile: fmt.Sprintf("FROM %s\nSYSTEM \"\"", createBinFile(t, nil, nil)),
|
||||
Stream: &stream,
|
||||
Name: "test",
|
||||
Files: map[string]string{"test.gguf": digest},
|
||||
System: "",
|
||||
Stream: &stream,
|
||||
})
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
|
@ -245,19 +275,9 @@ func TestCreateUnsetsSystem(t *testing.T) {
|
|||
})
|
||||
|
||||
checkFileExists(t, filepath.Join(p, "blobs", "*"), []string{
|
||||
filepath.Join(p, "blobs", "sha256-67d4b8d106af2a5b100a46e9bdc038c71eef2a35c9abac784092654212f97cf5"),
|
||||
filepath.Join(p, "blobs", "sha256-a4e5e156ddec27e286f75328784d7106b60a4eb1d246e950a001a3f944fbda99"),
|
||||
filepath.Join(p, "blobs", "sha256-e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"),
|
||||
filepath.Join(p, "blobs", "sha256-ca239d7bd8ea90e4a5d2e6bf88f8d74a47b14336e73eb4e18bed4dd325018116"),
|
||||
})
|
||||
|
||||
bts, err := os.ReadFile(filepath.Join(p, "blobs", "sha256-e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if string(bts) != "" {
|
||||
t.Fatalf("expected empty string, actual %s", string(bts))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateMergeParameters(t *testing.T) {
|
||||
|
@ -267,10 +287,16 @@ func TestCreateMergeParameters(t *testing.T) {
|
|||
t.Setenv("OLLAMA_MODELS", p)
|
||||
var s Server
|
||||
|
||||
_, digest := createBinFile(t, nil, nil)
|
||||
w := createRequest(t, s.CreateHandler, api.CreateRequest{
|
||||
Name: "test",
|
||||
Modelfile: fmt.Sprintf("FROM %s\nPARAMETER temperature 1\nPARAMETER top_k 10\nPARAMETER stop USER:\nPARAMETER stop ASSISTANT:", createBinFile(t, nil, nil)),
|
||||
Stream: &stream,
|
||||
Name: "test",
|
||||
Files: map[string]string{"test.gguf": digest},
|
||||
Parameters: map[string]any{
|
||||
"temperature": 1,
|
||||
"top_k": 10,
|
||||
"stop": []string{"USER:", "ASSISTANT:"},
|
||||
},
|
||||
Stream: &stream,
|
||||
})
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
|
@ -289,9 +315,13 @@ func TestCreateMergeParameters(t *testing.T) {
|
|||
|
||||
// in order to merge parameters, the second model must be created FROM the first
|
||||
w = createRequest(t, s.CreateHandler, api.CreateRequest{
|
||||
Name: "test2",
|
||||
Modelfile: "FROM test\nPARAMETER temperature 0.6\nPARAMETER top_p 0.7",
|
||||
Stream: &stream,
|
||||
Name: "test2",
|
||||
From: "test",
|
||||
Parameters: map[string]any{
|
||||
"temperature": 0.6,
|
||||
"top_p": 0.7,
|
||||
},
|
||||
Stream: &stream,
|
||||
})
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
|
@ -303,6 +333,22 @@ func TestCreateMergeParameters(t *testing.T) {
|
|||
filepath.Join(p, "manifests", "registry.ollama.ai", "library", "test2", "latest"),
|
||||
})
|
||||
|
||||
// Display contents of each blob in the directory
|
||||
blobDir := filepath.Join(p, "blobs")
|
||||
entries, err := os.ReadDir(blobDir)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read blobs directory: %v", err)
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
blobPath := filepath.Join(blobDir, entry.Name())
|
||||
content, err := os.ReadFile(blobPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read blob %s: %v", entry.Name(), err)
|
||||
}
|
||||
t.Logf("Contents of %s:\n%s", entry.Name(), string(content))
|
||||
}
|
||||
|
||||
checkFileExists(t, filepath.Join(p, "blobs", "*"), []string{
|
||||
filepath.Join(p, "blobs", "sha256-1d0ad71299d48c2fb7ae2b98e683643e771f8a5b72be34942af90d97a91c1e37"),
|
||||
filepath.Join(p, "blobs", "sha256-4a384beaf47a9cbe452dfa5ab70eea691790f3b35a832d12933a1996685bf2b6"),
|
||||
|
@ -327,9 +373,14 @@ func TestCreateMergeParameters(t *testing.T) {
|
|||
|
||||
// slices are replaced
|
||||
w = createRequest(t, s.CreateHandler, api.CreateRequest{
|
||||
Name: "test2",
|
||||
Modelfile: "FROM test\nPARAMETER temperature 0.6\nPARAMETER top_p 0.7\nPARAMETER stop <|endoftext|>",
|
||||
Stream: &stream,
|
||||
Name: "test2",
|
||||
From: "test",
|
||||
Parameters: map[string]any{
|
||||
"temperature": 0.6,
|
||||
"top_p": 0.7,
|
||||
"stop": []string{"<|endoftext|>"},
|
||||
},
|
||||
Stream: &stream,
|
||||
})
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
|
@ -371,10 +422,25 @@ func TestCreateReplacesMessages(t *testing.T) {
|
|||
t.Setenv("OLLAMA_MODELS", p)
|
||||
var s Server
|
||||
|
||||
_, digest := createBinFile(t, nil, nil)
|
||||
w := createRequest(t, s.CreateHandler, api.CreateRequest{
|
||||
Name: "test",
|
||||
Modelfile: fmt.Sprintf("FROM %s\nMESSAGE assistant \"What is my purpose?\"\nMESSAGE user \"You run tests.\"\nMESSAGE assistant \"Oh, my god.\"", createBinFile(t, nil, nil)),
|
||||
Stream: &stream,
|
||||
Name: "test",
|
||||
Files: map[string]string{"test.gguf": digest},
|
||||
Messages: []api.Message{
|
||||
{
|
||||
Role: "assistant",
|
||||
Content: "What is my purpose?",
|
||||
},
|
||||
{
|
||||
Role: "user",
|
||||
Content: "You run tests.",
|
||||
},
|
||||
{
|
||||
Role: "assistant",
|
||||
Content: "Oh, my god.",
|
||||
},
|
||||
},
|
||||
Stream: &stream,
|
||||
})
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
|
@ -392,9 +458,23 @@ func TestCreateReplacesMessages(t *testing.T) {
|
|||
})
|
||||
|
||||
w = createRequest(t, s.CreateHandler, api.CreateRequest{
|
||||
Name: "test2",
|
||||
Modelfile: "FROM test\nMESSAGE assistant \"You're a test, Harry.\"\nMESSAGE user \"I-I'm a what?\"\nMESSAGE assistant \"A test. And a thumping good one at that, I'd wager.\"",
|
||||
Stream: &stream,
|
||||
Name: "test2",
|
||||
From: "test",
|
||||
Messages: []api.Message{
|
||||
{
|
||||
Role: "assistant",
|
||||
Content: "You're a test, Harry.",
|
||||
},
|
||||
{
|
||||
Role: "user",
|
||||
Content: "I-I'm a what?",
|
||||
},
|
||||
{
|
||||
Role: "assistant",
|
||||
Content: "A test. And a thumping good one at that, I'd wager.",
|
||||
},
|
||||
},
|
||||
Stream: &stream,
|
||||
})
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
|
@ -406,12 +486,13 @@ func TestCreateReplacesMessages(t *testing.T) {
|
|||
filepath.Join(p, "manifests", "registry.ollama.ai", "library", "test2", "latest"),
|
||||
})
|
||||
|
||||
// Old layers will not have been pruned
|
||||
checkFileExists(t, filepath.Join(p, "blobs", "*"), []string{
|
||||
filepath.Join(p, "blobs", "sha256-298baeaf6928a60cf666d88d64a1ba606feb43a2865687c39e40652e407bffc4"),
|
||||
filepath.Join(p, "blobs", "sha256-4f48b25fe9969564c82f58eb1cedbdff6484cc0baf474bc6c2a9b37c8da3362a"),
|
||||
filepath.Join(p, "blobs", "sha256-a4e5e156ddec27e286f75328784d7106b60a4eb1d246e950a001a3f944fbda99"),
|
||||
filepath.Join(p, "blobs", "sha256-a60ecc9da299ec7ede453f99236e5577fd125e143689b646d9f0ddc9971bf4db"),
|
||||
filepath.Join(p, "blobs", "sha256-e0e27d47045063ccb167ae852c51d49a98eab33fabaee4633fdddf97213e40b5"),
|
||||
filepath.Join(p, "blobs", "sha256-f4e2c3690efef1b4b63ba1e1b2744ffeb6a7438a0110b86596069f6d9999c80b"),
|
||||
})
|
||||
|
||||
type message struct {
|
||||
|
@ -448,10 +529,13 @@ func TestCreateTemplateSystem(t *testing.T) {
|
|||
t.Setenv("OLLAMA_MODELS", p)
|
||||
var s Server
|
||||
|
||||
_, digest := createBinFile(t, nil, nil)
|
||||
w := createRequest(t, s.CreateHandler, api.CreateRequest{
|
||||
Name: "test",
|
||||
Modelfile: fmt.Sprintf("FROM %s\nTEMPLATE {{ .Prompt }}\nSYSTEM Say hello!\nTEMPLATE {{ .System }} {{ .Prompt }}\nSYSTEM Say bye!", createBinFile(t, nil, nil)),
|
||||
Stream: &stream,
|
||||
Name: "test",
|
||||
Files: map[string]string{"test.gguf": digest},
|
||||
Template: "{{ .System }} {{ .Prompt }}",
|
||||
System: "Say bye!",
|
||||
Stream: &stream,
|
||||
})
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
|
@ -488,10 +572,12 @@ func TestCreateTemplateSystem(t *testing.T) {
|
|||
}
|
||||
|
||||
t.Run("incomplete template", func(t *testing.T) {
|
||||
_, digest := createBinFile(t, nil, nil)
|
||||
w := createRequest(t, s.CreateHandler, api.CreateRequest{
|
||||
Name: "test",
|
||||
Modelfile: fmt.Sprintf("FROM %s\nTEMPLATE {{ .Prompt", createBinFile(t, nil, nil)),
|
||||
Stream: &stream,
|
||||
Name: "test",
|
||||
Files: map[string]string{"test.gguf": digest},
|
||||
Template: "{{ .Prompt",
|
||||
Stream: &stream,
|
||||
})
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
|
@ -500,10 +586,12 @@ func TestCreateTemplateSystem(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("template with unclosed if", func(t *testing.T) {
|
||||
_, digest := createBinFile(t, nil, nil)
|
||||
w := createRequest(t, s.CreateHandler, api.CreateRequest{
|
||||
Name: "test",
|
||||
Modelfile: fmt.Sprintf("FROM %s\nTEMPLATE {{ if .Prompt }}", createBinFile(t, nil, nil)),
|
||||
Stream: &stream,
|
||||
Name: "test",
|
||||
Files: map[string]string{"test.gguf": digest},
|
||||
Template: "{{ if .Prompt }}",
|
||||
Stream: &stream,
|
||||
})
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
|
@ -512,10 +600,12 @@ func TestCreateTemplateSystem(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("template with undefined function", func(t *testing.T) {
|
||||
_, digest := createBinFile(t, nil, nil)
|
||||
w := createRequest(t, s.CreateHandler, api.CreateRequest{
|
||||
Name: "test",
|
||||
Modelfile: fmt.Sprintf("FROM %s\nTEMPLATE {{ Prompt }}", createBinFile(t, nil, nil)),
|
||||
Stream: &stream,
|
||||
Name: "test",
|
||||
Files: map[string]string{"test.gguf": digest},
|
||||
Template: "{{ Prompt }}",
|
||||
Stream: &stream,
|
||||
})
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
|
@ -531,10 +621,12 @@ func TestCreateLicenses(t *testing.T) {
|
|||
t.Setenv("OLLAMA_MODELS", p)
|
||||
var s Server
|
||||
|
||||
_, digest := createBinFile(t, nil, nil)
|
||||
w := createRequest(t, s.CreateHandler, api.CreateRequest{
|
||||
Name: "test",
|
||||
Modelfile: fmt.Sprintf("FROM %s\nLICENSE MIT\nLICENSE Apache-2.0", createBinFile(t, nil, nil)),
|
||||
Stream: &stream,
|
||||
Name: "test",
|
||||
Files: map[string]string{"test.gguf": digest},
|
||||
License: []string{"MIT", "Apache-2.0"},
|
||||
Stream: &stream,
|
||||
})
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
|
@ -579,11 +671,12 @@ func TestCreateDetectTemplate(t *testing.T) {
|
|||
var s Server
|
||||
|
||||
t.Run("matched", func(t *testing.T) {
|
||||
_, digest := createBinFile(t, llm.KV{
|
||||
"tokenizer.chat_template": "{{ bos_token }}{% for message in messages %}{{'<|' + message['role'] + '|>' + '\n' + message['content'] + '<|end|>\n' }}{% endfor %}{% if add_generation_prompt %}{{ '<|assistant|>\n' }}{% else %}{{ eos_token }}{% endif %}",
|
||||
}, nil)
|
||||
w := createRequest(t, s.CreateHandler, api.CreateRequest{
|
||||
Name: "test",
|
||||
Modelfile: fmt.Sprintf("FROM %s", createBinFile(t, llm.KV{
|
||||
"tokenizer.chat_template": "{{ bos_token }}{% for message in messages %}{{'<|' + message['role'] + '|>' + '\n' + message['content'] + '<|end|>\n' }}{% endfor %}{% if add_generation_prompt %}{{ '<|assistant|>\n' }}{% else %}{{ eos_token }}{% endif %}",
|
||||
}, nil)),
|
||||
Name: "test",
|
||||
Files: map[string]string{"test.gguf": digest},
|
||||
Stream: &stream,
|
||||
})
|
||||
|
||||
|
@ -600,10 +693,11 @@ func TestCreateDetectTemplate(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("unmatched", func(t *testing.T) {
|
||||
_, digest := createBinFile(t, nil, nil)
|
||||
w := createRequest(t, s.CreateHandler, api.CreateRequest{
|
||||
Name: "test",
|
||||
Modelfile: fmt.Sprintf("FROM %s", createBinFile(t, nil, nil)),
|
||||
Stream: &stream,
|
||||
Name: "test",
|
||||
Files: map[string]string{"test.gguf": digest},
|
||||
Stream: &stream,
|
||||
})
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue