diff --git a/cmd/create.go b/cmd/create.go index 01efed949..59f60b5af 100644 --- a/cmd/create.go +++ b/cmd/create.go @@ -3,6 +3,7 @@ package cmd import ( "crypto/sha256" "encoding/hex" + "errors" "fmt" "io" "io/fs" @@ -200,14 +201,23 @@ func createRequest(modelfile *parser.Modelfile, dir string) (*api.CreateRequest, return nil, err } - if stat, err := os.Stat(path); err != nil { + fsys := os.DirFS(path) + seq := filesSeq(fsys) + if fi, err := os.Stat(path); errors.Is(err, os.ErrNotExist) { + m["from"] = cmd.Args + break + } else if err != nil { return nil, err - } else if !stat.IsDir() { - return nil, nil + } else if !fi.IsDir() { + base := filepath.Base(path) + path = filepath.Dir(path) + seq = func(yield func(string) bool) { + yield(base) + } } var mu sync.Mutex - for file := range filesSeq(os.DirFS(path)) { + for file := range seq { g.Go(func() error { f, err := os.Open(filepath.Join(path, file)) if err != nil { @@ -281,15 +291,16 @@ func createRequest(modelfile *parser.Modelfile, dir string) (*api.CreateRequest, } return &api.CreateRequest{ + From: get[string](m, "from"), Files: files, Adapters: adapters, - Parameters: parameters, - Template: get[string](m, "template"), - System: get[string](m, "system"), License: get[[]string](m, "license"), Messages: get[[]api.Message](m, "message"), - Renderer: get[string](m, "renderer"), + Parameters: parameters, Parser: get[string](m, "parser"), + Renderer: get[string](m, "renderer"), + System: get[string](m, "system"), + Template: get[string](m, "template"), }, nil } diff --git a/cmd/create_test.go b/cmd/create_test.go new file mode 100644 index 000000000..61efb0cac --- /dev/null +++ b/cmd/create_test.go @@ -0,0 +1,258 @@ +package cmd + +import ( + "crypto/sha256" + "encoding/hex" + "errors" + "io" + "os" + "os/user" + "path/filepath" + "runtime" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/ollama/ollama/api" + "github.com/ollama/ollama/fs/ggml" + "github.com/ollama/ollama/parser" +) + +func TestCreateRequest(t *testing.T) { + cases := []struct { + modelfile parser.Modelfile + expected *api.CreateRequest + }{ + { + parser.Modelfile{ + Commands: []parser.Command{ + {Name: "model", Args: "test"}, + }, + }, + &api.CreateRequest{ + From: "test", + License: []string(nil), + }, + }, + { + parser.Modelfile{ + Commands: []parser.Command{ + {Name: "model", Args: "test"}, + {Name: "template", Args: "some template"}, + }, + }, + &api.CreateRequest{ + From: "test", + Template: "some template", + License: []string(nil), + }, + }, + { + parser.Modelfile{ + Commands: []parser.Command{ + {Name: "model", Args: "test"}, + {Name: "license", Args: "single license"}, + {Name: "temperature", Args: "0.5"}, + {Name: "message", Args: "user: Hello"}, + }, + }, + &api.CreateRequest{ + From: "test", + License: []string{"single license"}, + Parameters: map[string]any{"temperature": float32(0.5)}, + Messages: []api.Message{ + {Role: "user", Content: "Hello"}, + }, + }, + }, + { + parser.Modelfile{ + Commands: []parser.Command{ + {Name: "model", Args: "test"}, + {Name: "temperature", Args: "0.5"}, + {Name: "top_k", Args: "1"}, + {Name: "system", Args: "You are a bot."}, + {Name: "license", Args: "license1"}, + {Name: "license", Args: "license2"}, + {Name: "message", Args: "user: Hello there!"}, + {Name: "message", Args: "assistant: Hi! How are you?"}, + }, + }, + &api.CreateRequest{ + From: "test", + License: []string{"license1", "license2"}, + System: "You are a bot.", + Parameters: map[string]any{"temperature": float32(0.5), "top_k": int64(1)}, + Messages: []api.Message{ + {Role: "user", Content: "Hello there!"}, + {Role: "assistant", Content: "Hi! How are you?"}, + }, + }, + }, + } + + for _, c := range cases { + actual, err := createRequest(&c.modelfile, "") + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff(actual, c.expected, + cmpopts.EquateEmpty(), + cmpopts.SortSlices(func(a, b api.File) bool { return a.Path < b.Path }), + ); diff != "" { + t.Errorf("mismatch (-got +want):\n%s", diff) + } + } +} + +func createBinFile(t *testing.T, d string, kv map[string]any, ti []*ggml.Tensor) (string, string) { + t.Helper() + + f, err := os.CreateTemp(d, "testbin.*.gguf") + if err != nil { + t.Fatal(err) + } + defer f.Close() + + if err := ggml.WriteGGUF(f, kv, ti); err != nil { + t.Fatal(err) + } + + // Calculate sha256 of file + if _, err := f.Seek(0, 0); err != nil { + t.Fatal(err) + } + + sha256sum := sha256.New() + if _, err := io.Copy(sha256sum, f); err != nil { + t.Fatal(err) + } + + return f.Name(), "sha256:" + hex.EncodeToString(sha256sum.Sum(nil)) +} + +func TestCreateRequestFiles(t *testing.T) { + d := t.TempDir() + n1, d1 := createBinFile(t, d, nil, nil) + n2, d2 := createBinFile(t, d, map[string]any{"foo": "bar"}, nil) + + cases := []struct { + modelfile parser.Modelfile + expected *api.CreateRequest + }{ + { + parser.Modelfile{ + Commands: []parser.Command{ + {Name: "model", Args: n1}, + }, + }, + &api.CreateRequest{ + Files: []api.File{ + { + Name: filepath.Base(n1), + Path: n1, + Digest: d1, + }, + }, + License: []string(nil), + }, + }, + { + parser.Modelfile{ + Commands: []parser.Command{ + {Name: "model", Args: n1}, + {Name: "model", Args: n2}, + }, + }, + &api.CreateRequest{ + Files: []api.File{ + { + Name: filepath.Base(n1), + Path: n1, + Digest: d1, + }, + { + Name: filepath.Base(n2), + Path: n2, + Digest: d2, + }, + }, + License: []string(nil), + }, + }, + } + + for _, c := range cases { + actual, err := createRequest(&c.modelfile, d) + if err != nil { + t.Error(err) + } + + if diff := cmp.Diff(actual, c.expected, + cmpopts.EquateEmpty(), + cmpopts.SortSlices(func(a, b api.File) bool { return a.Path < b.Path }), + ); diff != "" { + t.Errorf("mismatch (-got +want):\n%s", diff) + } + } +} + +func TestExpandPath(t *testing.T) { + home := t.TempDir() + t.Setenv("HOME", home) + t.Setenv("USERPROFILE", home) + + cwd, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + + u, err := user.Current() + if err != nil { + t.Fatal(err) + } + + volume := "" + if runtime.GOOS == "windows" { + volume = "D:" + } + + cases := []struct { + input, + dir, + want string + err error + }{ + {"~", "", home, nil}, + {"~/path/to/file", "", filepath.Join(home, filepath.ToSlash("path/to/file")), nil}, + {"~" + u.Username + "/path/to/file", "", filepath.Join(u.HomeDir, filepath.ToSlash("path/to/file")), nil}, + {"~nonexistentuser/path/to/file", "", "", user.UnknownUserError("nonexistentuser")}, + {"relative/path/to/file", "", filepath.Join(cwd, filepath.ToSlash("relative/path/to/file")), nil}, + {volume + "/absolute/path/to/file", "", filepath.ToSlash(volume + "/absolute/path/to/file"), nil}, + {volume + "/absolute/path/to/file", filepath.ToSlash("another/path"), filepath.ToSlash(volume + "/absolute/path/to/file"), nil}, + {".", cwd, cwd, nil}, + {".", "", cwd, nil}, + {"", cwd, cwd, nil}, + {"", "", cwd, nil}, + {"file", "path/to", filepath.Join(cwd, filepath.ToSlash("path/to/file")), nil}, + } + + for _, tt := range cases { + t.Run(tt.input, func(t *testing.T) { + got, err := expandPath(tt.input, tt.dir) + // On Windows, user.Lookup does not map syscall errors to user.UnknownUserError + // so we special case the test to just check for an error. + // See https://cs.opensource.google/go/go/+/refs/tags/go1.25.1:src/os/user/lookup_windows.go;l=455 + if runtime.GOOS != "windows" && !errors.Is(err, tt.err) { + t.Fatalf("expandPath(%q) error = %v, wantErr %v", tt.input, err, tt.err) + } else if tt.err != nil && err == nil { + t.Fatal("test case expected to fail on windows") + } + + if got != tt.want { + t.Errorf("expandPath(%q) = %v, want %v", tt.input, got, tt.want) + } + }) + } +} diff --git a/parser/parser_test.go b/parser/parser_test.go index 9de92ba21..820f2fef7 100644 --- a/parser/parser_test.go +++ b/parser/parser_test.go @@ -2,27 +2,18 @@ package parser import ( "bytes" - "crypto/sha256" "encoding/binary" "errors" "fmt" "io" - "os" - "os/user" - "path/filepath" - "runtime" "strings" "testing" "unicode/utf16" - "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/text/encoding" "golang.org/x/text/encoding/unicode" - - "github.com/ollama/ollama/api" - "github.com/ollama/ollama/fs/ggml" ) func TestParseFileFile(t *testing.T) { @@ -702,214 +693,3 @@ func TestParseMultiByte(t *testing.T) { }) } } - -func TestCreateRequest(t *testing.T) { - cases := []struct { - input string - expected *api.CreateRequest - }{ - { - `FROM test`, - &api.CreateRequest{From: "test"}, - }, - { - `FROM test -TEMPLATE some template -`, - &api.CreateRequest{ - From: "test", - Template: "some template", - }, - }, - { - `FROM test -LICENSE single license -PARAMETER temperature 0.5 -MESSAGE user Hello -`, - &api.CreateRequest{ - From: "test", - License: []string{"single license"}, - Parameters: map[string]any{"temperature": float32(0.5)}, - Messages: []api.Message{ - {Role: "user", Content: "Hello"}, - }, - }, - }, - { - `FROM test -PARAMETER temperature 0.5 -PARAMETER top_k 1 -SYSTEM You are a bot. -LICENSE license1 -LICENSE license2 -MESSAGE user Hello there! -MESSAGE assistant Hi! How are you? -`, - &api.CreateRequest{ - From: "test", - License: []string{"license1", "license2"}, - System: "You are a bot.", - Parameters: map[string]any{"temperature": float32(0.5), "top_k": int64(1)}, - Messages: []api.Message{ - {Role: "user", Content: "Hello there!"}, - {Role: "assistant", Content: "Hi! How are you?"}, - }, - }, - }, - } - - for _, c := range cases { - s, err := unicode.UTF8.NewEncoder().String(c.input) - if err != nil { - t.Fatal(err) - } - - p, err := ParseFile(strings.NewReader(s)) - if err != nil { - t.Error(err) - } - - actual, err := p.CreateRequest("") - if err != nil { - t.Error(err) - } - - if diff := cmp.Diff(actual, c.expected); diff != "" { - t.Errorf("mismatch (-got +want):\n%s", diff) - } - } -} - -func getSHA256Digest(t *testing.T, r io.Reader) (string, int64) { - t.Helper() - - h := sha256.New() - n, err := io.Copy(h, r) - if err != nil { - t.Fatal(err) - } - - return fmt.Sprintf("sha256:%x", h.Sum(nil)), n -} - -func createBinFile(t *testing.T, kv map[string]any, ti []*ggml.Tensor) (string, string) { - t.Helper() - - f, err := os.CreateTemp(t.TempDir(), "testbin.*.gguf") - if err != nil { - t.Fatal(err) - } - defer f.Close() - - if err := ggml.WriteGGUF(f, kv, ti); err != nil { - t.Fatal(err) - } - // Calculate sha256 of file - if _, err := f.Seek(0, 0); err != nil { - t.Fatal(err) - } - - digest, _ := getSHA256Digest(t, f) - - return f.Name(), digest -} - -func TestCreateRequestFiles(t *testing.T) { - n1, d1 := createBinFile(t, nil, nil) - n2, d2 := createBinFile(t, map[string]any{"foo": "bar"}, nil) - - cases := []struct { - input string - expected *api.CreateRequest - }{ - { - fmt.Sprintf("FROM %s", n1), - &api.CreateRequest{Files: map[string]string{n1: d1}}, - }, - { - fmt.Sprintf("FROM %s\nFROM %s", n1, n2), - &api.CreateRequest{Files: map[string]string{n1: d1, n2: d2}}, - }, - } - - for _, c := range cases { - s, err := unicode.UTF8.NewEncoder().String(c.input) - if err != nil { - t.Fatal(err) - } - - p, err := ParseFile(strings.NewReader(s)) - if err != nil { - t.Error(err) - } - - actual, err := p.CreateRequest("") - if err != nil { - t.Error(err) - } - - if diff := cmp.Diff(actual, c.expected); diff != "" { - t.Errorf("mismatch (-got +want):\n%s", diff) - } - } -} - -func TestExpandPath(t *testing.T) { - home := t.TempDir() - t.Setenv("HOME", home) - t.Setenv("USERPROFILE", home) - - cwd, err := os.Getwd() - if err != nil { - t.Fatal(err) - } - - u, err := user.Current() - if err != nil { - t.Fatal(err) - } - - volume := "" - if runtime.GOOS == "windows" { - volume = "D:" - } - - cases := []struct { - input, - dir, - want string - err error - }{ - {"~", "", home, nil}, - {"~/path/to/file", "", filepath.Join(home, filepath.ToSlash("path/to/file")), nil}, - {"~" + u.Username + "/path/to/file", "", filepath.Join(u.HomeDir, filepath.ToSlash("path/to/file")), nil}, - {"~nonexistentuser/path/to/file", "", "", user.UnknownUserError("nonexistentuser")}, - {"relative/path/to/file", "", filepath.Join(cwd, filepath.ToSlash("relative/path/to/file")), nil}, - {volume + "/absolute/path/to/file", "", filepath.ToSlash(volume + "/absolute/path/to/file"), nil}, - {volume + "/absolute/path/to/file", filepath.ToSlash("another/path"), filepath.ToSlash(volume + "/absolute/path/to/file"), nil}, - {".", cwd, cwd, nil}, - {".", "", cwd, nil}, - {"", cwd, cwd, nil}, - {"", "", cwd, nil}, - {"file", "path/to", filepath.Join(cwd, filepath.ToSlash("path/to/file")), nil}, - } - - for _, tt := range cases { - t.Run(tt.input, func(t *testing.T) { - got, err := expandPath(tt.input, tt.dir) - // On Windows, user.Lookup does not map syscall errors to user.UnknownUserError - // so we special case the test to just check for an error. - // See https://cs.opensource.google/go/go/+/refs/tags/go1.25.1:src/os/user/lookup_windows.go;l=455 - if runtime.GOOS != "windows" && !errors.Is(err, tt.err) { - t.Fatalf("expandPath(%q) error = %v, wantErr %v", tt.input, err, tt.err) - } else if tt.err != nil && err == nil { - t.Fatal("test case expected to fail on windows") - } - - if got != tt.want { - t.Errorf("expandPath(%q) = %v, want %v", tt.input, got, tt.want) - } - }) - } -} diff --git a/server/create_test.go b/server/create_test.go index 061efb81a..745595d62 100644 --- a/server/create_test.go +++ b/server/create_test.go @@ -88,10 +88,10 @@ func TestConvertFromSafetensors(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Create the minimum required file map for convertFromSafetensors - files := map[string]string{ - tt.filePath: model, - "config.json": config, - "tokenizer.json": tokenizer, + files := []api.File{ + {Name: tt.filePath, Digest: model}, + {Name: "config.json", Digest: config}, + {Name: "tokenizer.json", Digest: tokenizer}, } _, err := convertFromSafetensors(files, nil, false, func(resp api.ProgressResponse) {}) diff --git a/server/routes_create_test.go b/server/routes_create_test.go index 189ef0407..3ea8a9c39 100644 --- a/server/routes_create_test.go +++ b/server/routes_create_test.go @@ -119,7 +119,7 @@ func TestCreateFromBin(t *testing.T) { w := createRequest(t, s.CreateHandler, api.CreateRequest{ Name: "test", - Files: map[string]string{"test.gguf": digest}, + Files: []api.File{{Name: "test.gguf", Digest: digest}}, Stream: &stream, }) @@ -149,7 +149,7 @@ func TestCreateFromModel(t *testing.T) { w := createRequest(t, s.CreateHandler, api.CreateRequest{ Name: "test", - Files: map[string]string{"test.gguf": digest}, + Files: []api.File{{Name: "test.gguf", Digest: digest}}, Stream: &stream, }) @@ -192,7 +192,7 @@ func TestCreateRemovesLayers(t *testing.T) { _, digest := createBinFile(t, nil, nil) w := createRequest(t, s.CreateHandler, api.CreateRequest{ Name: "test", - Files: map[string]string{"test.gguf": digest}, + Files: []api.File{{Name: "test.gguf", Digest: digest}}, Template: "{{ .Prompt }}", Stream: &stream, }) @@ -213,7 +213,7 @@ func TestCreateRemovesLayers(t *testing.T) { w = createRequest(t, s.CreateHandler, api.CreateRequest{ Name: "test", - Files: map[string]string{"test.gguf": digest}, + Files: []api.File{{Name: "test.gguf", Digest: digest}}, Template: "{{ .System }} {{ .Prompt }}", Stream: &stream, }) @@ -243,7 +243,7 @@ func TestCreateUnsetsSystem(t *testing.T) { _, digest := createBinFile(t, nil, nil) w := createRequest(t, s.CreateHandler, api.CreateRequest{ Name: "test", - Files: map[string]string{"test.gguf": digest}, + Files: []api.File{{Name: "test.gguf", Digest: digest}}, System: "Say hi!", Stream: &stream, }) @@ -264,7 +264,7 @@ func TestCreateUnsetsSystem(t *testing.T) { w = createRequest(t, s.CreateHandler, api.CreateRequest{ Name: "test", - Files: map[string]string{"test.gguf": digest}, + Files: []api.File{{Name: "test.gguf", Digest: digest}}, System: "", Stream: &stream, }) @@ -293,7 +293,7 @@ func TestCreateMergeParameters(t *testing.T) { _, digest := createBinFile(t, nil, nil) w := createRequest(t, s.CreateHandler, api.CreateRequest{ Name: "test", - Files: map[string]string{"test.gguf": digest}, + Files: []api.File{{Name: "test.gguf", Digest: digest}}, Parameters: map[string]any{ "temperature": 1, "top_k": 10, @@ -428,7 +428,7 @@ func TestCreateReplacesMessages(t *testing.T) { _, digest := createBinFile(t, nil, nil) w := createRequest(t, s.CreateHandler, api.CreateRequest{ Name: "test", - Files: map[string]string{"test.gguf": digest}, + Files: []api.File{{Name: "test.gguf", Digest: digest}}, Messages: []api.Message{ { Role: "assistant", @@ -535,7 +535,7 @@ func TestCreateTemplateSystem(t *testing.T) { _, digest := createBinFile(t, nil, nil) w := createRequest(t, s.CreateHandler, api.CreateRequest{ Name: "test", - Files: map[string]string{"test.gguf": digest}, + Files: []api.File{{Name: "test.gguf", Digest: digest}}, Template: "{{ .System }} {{ .Prompt }}", System: "Say bye!", Stream: &stream, @@ -578,7 +578,7 @@ func TestCreateTemplateSystem(t *testing.T) { _, digest := createBinFile(t, nil, nil) w := createRequest(t, s.CreateHandler, api.CreateRequest{ Name: "test", - Files: map[string]string{"test.gguf": digest}, + Files: []api.File{{Name: "test.gguf", Digest: digest}}, Template: "{{ .Prompt", Stream: &stream, }) @@ -592,7 +592,7 @@ func TestCreateTemplateSystem(t *testing.T) { _, digest := createBinFile(t, nil, nil) w := createRequest(t, s.CreateHandler, api.CreateRequest{ Name: "test", - Files: map[string]string{"test.gguf": digest}, + Files: []api.File{{Name: "test.gguf", Digest: digest}}, Template: "{{ if .Prompt }}", Stream: &stream, }) @@ -606,7 +606,7 @@ func TestCreateTemplateSystem(t *testing.T) { _, digest := createBinFile(t, nil, nil) w := createRequest(t, s.CreateHandler, api.CreateRequest{ Name: "test", - Files: map[string]string{"test.gguf": digest}, + Files: []api.File{{Name: "test.gguf", Digest: digest}}, Template: "{{ Prompt }}", Stream: &stream, }) @@ -699,7 +699,7 @@ func TestCreateLicenses(t *testing.T) { _, digest := createBinFile(t, nil, nil) w := createRequest(t, s.CreateHandler, api.CreateRequest{ Name: "test", - Files: map[string]string{"test.gguf": digest}, + Files: []api.File{{Name: "test.gguf", Digest: digest}}, License: []string{"MIT", "Apache-2.0"}, Stream: &stream, }) @@ -751,7 +751,7 @@ func TestCreateDetectTemplate(t *testing.T) { }, nil) w := createRequest(t, s.CreateHandler, api.CreateRequest{ Name: "test", - Files: map[string]string{"test.gguf": digest}, + Files: []api.File{{Name: "test.gguf", Digest: digest}}, Stream: &stream, }) @@ -771,7 +771,7 @@ func TestCreateDetectTemplate(t *testing.T) { _, digest := createBinFile(t, nil, nil) w := createRequest(t, s.CreateHandler, api.CreateRequest{ Name: "test", - Files: map[string]string{"test.gguf": digest}, + Files: []api.File{{Name: "test.gguf", Digest: digest}}, Stream: &stream, }) @@ -789,9 +789,7 @@ func TestCreateDetectTemplate(t *testing.T) { func TestDetectModelTypeFromFiles(t *testing.T) { t.Run("gguf file", func(t *testing.T) { _, digest := createBinFile(t, nil, nil) - files := map[string]string{ - "model.gguf": digest, - } + files := []api.File{{Name: "model.gguf", Digest: digest}} modelType := detectModelTypeFromFiles(files) if modelType != "gguf" { @@ -801,8 +799,8 @@ func TestDetectModelTypeFromFiles(t *testing.T) { t.Run("gguf file w/o extension", func(t *testing.T) { _, digest := createBinFile(t, nil, nil) - files := map[string]string{ - fmt.Sprintf("%x", digest): digest, + files := []api.File{ + {Name: fmt.Sprintf("%x", digest), Digest: digest}, } modelType := detectModelTypeFromFiles(files) @@ -812,8 +810,8 @@ func TestDetectModelTypeFromFiles(t *testing.T) { }) t.Run("safetensors file", func(t *testing.T) { - files := map[string]string{ - "model.safetensors": "sha256:abc123", + files := []api.File{ + {Name: "model.safetensors", Digest: "sha256:abc123"}, } modelType := detectModelTypeFromFiles(files) @@ -842,8 +840,8 @@ func TestDetectModelTypeFromFiles(t *testing.T) { t.Fatal(err) } - files := map[string]string{ - "model.bin": digest, + files := []api.File{ + {Name: "model.bin", Digest: digest}, } modelType := detectModelTypeFromFiles(files) @@ -872,8 +870,8 @@ func TestDetectModelTypeFromFiles(t *testing.T) { t.Fatal(err) } - files := map[string]string{ - "noext": digest, + files := []api.File{ + {Name: "noext", Digest: digest}, } modelType := detectModelTypeFromFiles(files) diff --git a/server/routes_debug_test.go b/server/routes_debug_test.go index 6507284ef..2b918d97b 100644 --- a/server/routes_debug_test.go +++ b/server/routes_debug_test.go @@ -80,7 +80,7 @@ func TestGenerateDebugRenderOnly(t *testing.T) { w := createRequest(t, s.CreateHandler, api.CreateRequest{ Model: "test-model", - Files: map[string]string{"file.gguf": digest}, + Files: []api.File{{Name: "file.gguf", Digest: digest}}, Template: "{{ .Prompt }}", Stream: &stream, }) @@ -273,7 +273,7 @@ func TestChatDebugRenderOnly(t *testing.T) { w := createRequest(t, s.CreateHandler, api.CreateRequest{ Model: "test-model", - Files: map[string]string{"file.gguf": digest}, + Files: []api.File{{Name: "file.gguf", Digest: digest}}, Template: "{{ if .Tools }}{{ .Tools }}{{ end }}{{ range .Messages }}{{ .Role }}: {{ .Content }}\n{{ end }}", Stream: &stream, }) diff --git a/server/routes_delete_test.go b/server/routes_delete_test.go index 2e00c08df..e6400d237 100644 --- a/server/routes_delete_test.go +++ b/server/routes_delete_test.go @@ -24,7 +24,7 @@ func TestDelete(t *testing.T) { _, digest := createBinFile(t, nil, nil) w := createRequest(t, s.CreateHandler, api.CreateRequest{ Name: "test", - Files: map[string]string{"test.gguf": digest}, + Files: []api.File{{Name: "test.gguf", Digest: digest}}, }) if w.Code != http.StatusOK { @@ -33,7 +33,7 @@ func TestDelete(t *testing.T) { w = createRequest(t, s.CreateHandler, api.CreateRequest{ Name: "test2", - Files: map[string]string{"test.gguf": digest}, + Files: []api.File{{Name: "test.gguf", Digest: digest}}, Template: "{{ .System }} {{ .Prompt }}", }) diff --git a/server/routes_generate_test.go b/server/routes_generate_test.go index a3b83fc1a..1b0b5c4c9 100644 --- a/server/routes_generate_test.go +++ b/server/routes_generate_test.go @@ -116,7 +116,7 @@ func TestGenerateChat(t *testing.T) { w := createRequest(t, s.CreateHandler, api.CreateRequest{ Model: "test", - Files: map[string]string{"file.gguf": digest}, + Files: []api.File{{Name: "file.gguf", Digest: digest}}, Template: ` {{- if .Tools }} {{ .Tools }} @@ -181,7 +181,7 @@ func TestGenerateChat(t *testing.T) { }, []*ggml.Tensor{}) w := createRequest(t, s.CreateHandler, api.CreateRequest{ Model: "bert", - Files: map[string]string{"bert.gguf": digest}, + Files: []api.File{{Name: "bert.gguf", Digest: digest}}, Stream: &stream, }) @@ -660,7 +660,7 @@ func TestGenerate(t *testing.T) { w := createRequest(t, s.CreateHandler, api.CreateRequest{ Model: "test", - Files: map[string]string{"file.gguf": digest}, + Files: []api.File{{Name: "file.gguf", Digest: digest}}, Template: ` {{- if .System }}System: {{ .System }} {{ end }} {{- if .Prompt }}User: {{ .Prompt }} {{ end }} @@ -703,7 +703,7 @@ func TestGenerate(t *testing.T) { w := createRequest(t, s.CreateHandler, api.CreateRequest{ Model: "bert", - Files: map[string]string{"file.gguf": digest}, + Files: []api.File{{Name: "file.gguf", Digest: digest}}, Stream: &stream, }) @@ -1035,7 +1035,7 @@ func TestChatWithPromptEndingInThinkTag(t *testing.T) { // Create model with thinking template that adds at the end w := createRequest(t, s.CreateHandler, api.CreateRequest{ Model: "test-thinking", - Files: map[string]string{"file.gguf": digest}, + Files: []api.File{{Name: "file.gguf", Digest: digest}}, Template: `{{- range .Messages }} {{- if eq .Role "user" }}user: {{ .Content }} {{ else if eq .Role "assistant" }}assistant: {{ if .Thinking }}{{ .Thinking }}{{ end }}{{ .Content }} diff --git a/server/routes_harmony_streaming_test.go b/server/routes_harmony_streaming_test.go index b1ede4e39..c3a37d97d 100644 --- a/server/routes_harmony_streaming_test.go +++ b/server/routes_harmony_streaming_test.go @@ -294,7 +294,7 @@ func TestChatHarmonyParserStreamingRealtime(t *testing.T) { streamFalse := false w := createRequest(t, s.CreateHandler, api.CreateRequest{ Model: "harmony-test-streaming", - Files: map[string]string{"test.gguf": digest}, + Files: []api.File{{Name: "test.gguf", Digest: digest}}, Template: `<|start|><|end|>{{ with .Tools }}{{ end }}{{ .Prompt }}`, Stream: &streamFalse, }) @@ -444,7 +444,7 @@ func TestChatHarmonyParserStreamingSimple(t *testing.T) { streamFalse := false w := createRequest(t, s.CreateHandler, api.CreateRequest{ Model: "gpt-oss", - Files: map[string]string{"test.gguf": digest}, + Files: []api.File{{Name: "test.gguf", Digest: digest}}, Template: `<|start|><|end|>{{ .Tools }}{{ .Prompt }}`, Stream: &streamFalse, }) @@ -628,7 +628,7 @@ func TestChatHarmonyParserStreaming(t *testing.T) { stream := false w := createRequest(t, s.CreateHandler, api.CreateRequest{ Model: "harmony-test", - Files: map[string]string{"file.gguf": digest}, + Files: []api.File{{Name: "file.gguf", Digest: digest}}, Template: `<|start|><|end|>{{ with .Tools }}{{ end }}{{ .Prompt }}`, Stream: &stream, }) diff --git a/server/routes_list_test.go b/server/routes_list_test.go index f6e899ad7..f4720f282 100644 --- a/server/routes_list_test.go +++ b/server/routes_list_test.go @@ -34,7 +34,7 @@ func TestList(t *testing.T) { createRequest(t, s.CreateHandler, api.CreateRequest{ Name: n, - Files: map[string]string{"test.gguf": digest}, + Files: []api.File{{Name: "test.gguf", Digest: digest}}, }) } diff --git a/server/routes_test.go b/server/routes_test.go index bb7e2b7c1..c4dcd1d28 100644 --- a/server/routes_test.go +++ b/server/routes_test.go @@ -111,7 +111,7 @@ func TestRoutes(t *testing.T) { r := api.CreateRequest{ Name: name, - Files: map[string]string{"test.gguf": digest}, + Files: []api.File{{Name: "test.gguf", Digest: digest}}, Parameters: map[string]any{ "seed": 42, "top_p": 0.9, @@ -343,7 +343,7 @@ func TestRoutes(t *testing.T) { stream := false createReq := api.CreateRequest{ Name: "t-bone", - Files: map[string]string{"test.gguf": digest}, + Files: []api.File{{Name: "test.gguf", Digest: digest}}, Stream: &stream, } jsonData, err := json.Marshal(createReq) @@ -645,7 +645,7 @@ func TestManifestCaseSensitivity(t *testing.T) { // Start with the stable name, and later use a case-shuffled // version. Name: wantStableName, - Files: map[string]string{"test.gguf": digest}, + Files: []api.File{{Name: "test.gguf", Digest: digest}}, Stream: &stream, })) checkManifestList() @@ -653,7 +653,7 @@ func TestManifestCaseSensitivity(t *testing.T) { t.Logf("creating (again)") checkOK(createRequest(t, s.CreateHandler, api.CreateRequest{ Name: name(), - Files: map[string]string{"test.gguf": digest}, + Files: []api.File{{Name: "test.gguf", Digest: digest}}, Stream: &stream, })) checkManifestList() @@ -696,7 +696,7 @@ func TestShow(t *testing.T) { createRequest(t, s.CreateHandler, api.CreateRequest{ Name: "show-model", - Files: map[string]string{"model.gguf": digest1, "projector.gguf": digest2}, + Files: []api.File{{Name: "model.gguf", Digest: digest1}, {Name: "projector.gguf", Digest: digest2}}, }) w := createRequest(t, s.ShowHandler, api.ShowRequest{