mirror of
https://github.com/ollama/ollama.git
synced 2026-01-19 04:51:17 -05:00
Compare commits
3 Commits
parth/decr
...
mxyng/fix-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ffaf2e7916 | ||
|
|
b846eacf42 | ||
|
|
220a0da37e |
33
api/types.go
33
api/types.go
@@ -471,10 +471,10 @@ type CreateRequest struct {
|
|||||||
RemoteHost string `json:"remote_host,omitempty"`
|
RemoteHost string `json:"remote_host,omitempty"`
|
||||||
|
|
||||||
// Files is a map of files include when creating the model.
|
// Files is a map of files include when creating the model.
|
||||||
Files map[string]string `json:"files,omitempty"`
|
Files Files `json:"files,omitempty"`
|
||||||
|
|
||||||
// Adapters is a map of LoRA adapters to include when creating the model.
|
// Adapters is a map of LoRA adapters to include when creating the model.
|
||||||
Adapters map[string]string `json:"adapters,omitempty"`
|
Adapters Files `json:"adapters,omitempty"`
|
||||||
|
|
||||||
// Template is the template used when constructing a request to the model.
|
// Template is the template used when constructing a request to the model.
|
||||||
Template string `json:"template,omitempty"`
|
Template string `json:"template,omitempty"`
|
||||||
@@ -503,6 +503,31 @@ type CreateRequest struct {
|
|||||||
Quantization string `json:"quantization,omitempty"`
|
Quantization string `json:"quantization,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Files []File
|
||||||
|
|
||||||
|
func (f Files) MarshalJSON() ([]byte, error) {
|
||||||
|
m := make(map[string]string, len(f))
|
||||||
|
for _, file := range f {
|
||||||
|
m[file.Name] = file.Digest
|
||||||
|
}
|
||||||
|
return json.Marshal(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Files) UnmarshalJSON(data []byte) error {
|
||||||
|
m := make(map[string]string)
|
||||||
|
if err := json.Unmarshal(data, &m); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for name, digest := range m {
|
||||||
|
*f = append(*f, File{Name: name, Digest: digest})
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type File struct {
|
||||||
|
Name, Path, Digest string
|
||||||
|
}
|
||||||
|
|
||||||
// DeleteRequest is the request passed to [Client.Delete].
|
// DeleteRequest is the request passed to [Client.Delete].
|
||||||
type DeleteRequest struct {
|
type DeleteRequest struct {
|
||||||
Model string `json:"model"`
|
Model string `json:"model"`
|
||||||
@@ -988,8 +1013,8 @@ func (d *Duration) UnmarshalJSON(b []byte) (err error) {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// FormatParams converts specified parameter options to their correct types
|
// FormatParameters converts specified parameter options to their correct types
|
||||||
func FormatParams(params map[string][]string) (map[string]any, error) {
|
func FormatParameters(params map[string][]string) (map[string]any, error) {
|
||||||
opts := Options{}
|
opts := Options{}
|
||||||
valueOpts := reflect.ValueOf(&opts).Elem() // names of the fields in the options struct
|
valueOpts := reflect.ValueOf(&opts).Elem() // names of the fields in the options struct
|
||||||
typeOpts := reflect.TypeOf(opts) // types of the fields in the options struct
|
typeOpts := reflect.TypeOf(opts) // types of the fields in the options struct
|
||||||
|
|||||||
@@ -203,7 +203,7 @@ func TestUseMmapFormatParams(t *testing.T) {
|
|||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
resp, err := FormatParams(test.req)
|
resp, err := FormatParameters(test.req)
|
||||||
require.Equal(t, test.err, err)
|
require.Equal(t, test.err, err)
|
||||||
respVal, ok := resp["use_mmap"]
|
respVal, ok := resp["use_mmap"]
|
||||||
if test.exp != nil {
|
if test.exp != nil {
|
||||||
|
|||||||
176
cmd/cmd.go
176
cmd/cmd.go
@@ -33,20 +33,17 @@ import (
|
|||||||
"github.com/olekukonko/tablewriter"
|
"github.com/olekukonko/tablewriter"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
"golang.org/x/sync/errgroup"
|
|
||||||
"golang.org/x/term"
|
"golang.org/x/term"
|
||||||
|
|
||||||
"github.com/ollama/ollama/api"
|
"github.com/ollama/ollama/api"
|
||||||
"github.com/ollama/ollama/auth"
|
"github.com/ollama/ollama/auth"
|
||||||
"github.com/ollama/ollama/envconfig"
|
"github.com/ollama/ollama/envconfig"
|
||||||
"github.com/ollama/ollama/format"
|
"github.com/ollama/ollama/format"
|
||||||
"github.com/ollama/ollama/parser"
|
|
||||||
"github.com/ollama/ollama/progress"
|
"github.com/ollama/ollama/progress"
|
||||||
"github.com/ollama/ollama/readline"
|
"github.com/ollama/ollama/readline"
|
||||||
"github.com/ollama/ollama/runner"
|
"github.com/ollama/ollama/runner"
|
||||||
"github.com/ollama/ollama/server"
|
"github.com/ollama/ollama/server"
|
||||||
"github.com/ollama/ollama/types/model"
|
"github.com/ollama/ollama/types/model"
|
||||||
"github.com/ollama/ollama/types/syncmap"
|
|
||||||
"github.com/ollama/ollama/version"
|
"github.com/ollama/ollama/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -89,179 +86,6 @@ func getModelfileName(cmd *cobra.Command) (string, error) {
|
|||||||
return absName, nil
|
return absName, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateHandler(cmd *cobra.Command, args []string) error {
|
|
||||||
p := progress.NewProgress(os.Stderr)
|
|
||||||
defer p.Stop()
|
|
||||||
|
|
||||||
var reader io.Reader
|
|
||||||
|
|
||||||
filename, err := getModelfileName(cmd)
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
if filename == "" {
|
|
||||||
reader = strings.NewReader("FROM .\n")
|
|
||||||
} else {
|
|
||||||
return errModelfileNotFound
|
|
||||||
}
|
|
||||||
} else if err != nil {
|
|
||||||
return err
|
|
||||||
} else {
|
|
||||||
f, err := os.Open(filename)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
reader = f
|
|
||||||
defer f.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
modelfile, err := parser.ParseFile(reader)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
status := "gathering model components"
|
|
||||||
spinner := progress.NewSpinner(status)
|
|
||||||
p.Add(status, spinner)
|
|
||||||
|
|
||||||
req, err := modelfile.CreateRequest(filepath.Dir(filename))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
spinner.Stop()
|
|
||||||
|
|
||||||
req.Model = args[0]
|
|
||||||
quantize, _ := cmd.Flags().GetString("quantize")
|
|
||||||
if quantize != "" {
|
|
||||||
req.Quantize = quantize
|
|
||||||
}
|
|
||||||
|
|
||||||
client, err := api.ClientFromEnvironment()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var g errgroup.Group
|
|
||||||
g.SetLimit(max(runtime.GOMAXPROCS(0)-1, 1))
|
|
||||||
|
|
||||||
files := syncmap.NewSyncMap[string, string]()
|
|
||||||
for f, digest := range req.Files {
|
|
||||||
g.Go(func() error {
|
|
||||||
if _, err := createBlob(cmd, client, f, digest, p); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: this is incorrect since the file might be in a subdirectory
|
|
||||||
// instead this should take the path relative to the model directory
|
|
||||||
// but the current implementation does not allow this
|
|
||||||
files.Store(filepath.Base(f), digest)
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
adapters := syncmap.NewSyncMap[string, string]()
|
|
||||||
for f, digest := range req.Adapters {
|
|
||||||
g.Go(func() error {
|
|
||||||
if _, err := createBlob(cmd, client, f, digest, p); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: same here
|
|
||||||
adapters.Store(filepath.Base(f), digest)
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := g.Wait(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
req.Files = files.Items()
|
|
||||||
req.Adapters = adapters.Items()
|
|
||||||
|
|
||||||
bars := make(map[string]*progress.Bar)
|
|
||||||
fn := func(resp api.ProgressResponse) error {
|
|
||||||
if resp.Digest != "" {
|
|
||||||
bar, ok := bars[resp.Digest]
|
|
||||||
if !ok {
|
|
||||||
msg := resp.Status
|
|
||||||
if msg == "" {
|
|
||||||
msg = fmt.Sprintf("pulling %s...", resp.Digest[7:19])
|
|
||||||
}
|
|
||||||
bar = progress.NewBar(msg, resp.Total, resp.Completed)
|
|
||||||
bars[resp.Digest] = bar
|
|
||||||
p.Add(resp.Digest, bar)
|
|
||||||
}
|
|
||||||
|
|
||||||
bar.Set(resp.Completed)
|
|
||||||
} else if status != resp.Status {
|
|
||||||
spinner.Stop()
|
|
||||||
|
|
||||||
status = resp.Status
|
|
||||||
spinner = progress.NewSpinner(status)
|
|
||||||
p.Add(status, spinner)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := client.Create(cmd.Context(), req, fn); err != nil {
|
|
||||||
if strings.Contains(err.Error(), "path or Modelfile are required") {
|
|
||||||
return fmt.Errorf("the ollama server must be updated to use `ollama create` with this client")
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func createBlob(cmd *cobra.Command, client *api.Client, path string, digest string, p *progress.Progress) (string, error) {
|
|
||||||
realPath, err := filepath.EvalSymlinks(path)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
bin, err := os.Open(realPath)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
defer bin.Close()
|
|
||||||
|
|
||||||
// Get file info to retrieve the size
|
|
||||||
fileInfo, err := bin.Stat()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
fileSize := fileInfo.Size()
|
|
||||||
|
|
||||||
var pw progressWriter
|
|
||||||
status := fmt.Sprintf("copying file %s 0%%", digest)
|
|
||||||
spinner := progress.NewSpinner(status)
|
|
||||||
p.Add(status, spinner)
|
|
||||||
defer spinner.Stop()
|
|
||||||
|
|
||||||
done := make(chan struct{})
|
|
||||||
defer close(done)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
ticker := time.NewTicker(60 * time.Millisecond)
|
|
||||||
defer ticker.Stop()
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ticker.C:
|
|
||||||
spinner.SetMessage(fmt.Sprintf("copying file %s %d%%", digest, int(100*pw.n.Load()/fileSize)))
|
|
||||||
case <-done:
|
|
||||||
spinner.SetMessage(fmt.Sprintf("copying file %s 100%%", digest))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
if err := client.CreateBlob(cmd.Context(), digest, io.TeeReader(bin, &pw)); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return digest, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type progressWriter struct {
|
type progressWriter struct {
|
||||||
n atomic.Int64
|
n atomic.Int64
|
||||||
}
|
}
|
||||||
|
|||||||
453
cmd/create.go
Normal file
453
cmd/create.go
Normal file
@@ -0,0 +1,453 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
|
"iter"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/user"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ollama/ollama/api"
|
||||||
|
"github.com/ollama/ollama/parser"
|
||||||
|
"github.com/ollama/ollama/progress"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
|
)
|
||||||
|
|
||||||
|
func expandPath(path, dir string) (string, error) {
|
||||||
|
if filepath.IsAbs(path) {
|
||||||
|
return path, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
path, found := strings.CutPrefix(path, "~")
|
||||||
|
if !found {
|
||||||
|
// make path relative to dir
|
||||||
|
if !filepath.IsAbs(dir) {
|
||||||
|
// if dir is relative, make it absolute relative to cwd
|
||||||
|
cwd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
dir = filepath.Join(cwd, dir)
|
||||||
|
}
|
||||||
|
path = filepath.Join(dir, path)
|
||||||
|
} else if filepath.IsLocal(path) {
|
||||||
|
// ~<user>/...
|
||||||
|
// make path relative to specified user's home
|
||||||
|
split := strings.SplitN(path, "/", 2)
|
||||||
|
u, err := user.Lookup(split[0])
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
split[0] = u.HomeDir
|
||||||
|
path = filepath.Join(split...)
|
||||||
|
} else {
|
||||||
|
// ~ or ~/...
|
||||||
|
// make path relative to current user's home
|
||||||
|
home, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
path = filepath.Join(home, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
return filepath.Clean(path), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func detectContentType(fsys fs.FS, path string) (string, error) {
|
||||||
|
f, err := fsys.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
b := make([]byte, 512)
|
||||||
|
if _, err := f.Read(b); err != nil && err != io.EOF {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
contentType, _, _ := strings.Cut(http.DetectContentType(b), ";")
|
||||||
|
return contentType, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// glob returns an iterator that yields files matching the given patterns and content types.
|
||||||
|
// The patterns and content types are provided as pairs of strings.
|
||||||
|
// If a content type is an empty string, all files matching the pattern are yielded.
|
||||||
|
// The iterator stops after the first pattern that matches any files.
|
||||||
|
func glob(fsys fs.FS, patternOrContentType ...string) iter.Seq2[string, error] {
|
||||||
|
if len(patternOrContentType)%2 != 0 {
|
||||||
|
panic("glob: patternOrContentType must have an even number of elements")
|
||||||
|
}
|
||||||
|
|
||||||
|
return func(yield func(string, error) bool) {
|
||||||
|
for i := 0; i < len(patternOrContentType); i += 2 {
|
||||||
|
pattern := patternOrContentType[i]
|
||||||
|
contentType := patternOrContentType[i+1]
|
||||||
|
|
||||||
|
matches, err := fs.Glob(fsys, pattern)
|
||||||
|
if err != nil {
|
||||||
|
yield("", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(matches) > 0 {
|
||||||
|
for _, match := range matches {
|
||||||
|
if contentType == "" {
|
||||||
|
if !yield(match, nil) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
ct, err := detectContentType(fsys, match)
|
||||||
|
if err != nil {
|
||||||
|
yield("", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ct == contentType {
|
||||||
|
if !yield(match, nil) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func filesSeq(fsys fs.FS) iter.Seq[string] {
|
||||||
|
return func(yield func(string) bool) {
|
||||||
|
for match := range glob(fsys,
|
||||||
|
"*.safetensors", "",
|
||||||
|
"*.bin", "application/zip",
|
||||||
|
"*.pth", "application/zip",
|
||||||
|
"*.gguf", "application/octet-stream",
|
||||||
|
"*.bin", "application/octet-stream") {
|
||||||
|
if !yield(match) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for match := range glob(fsys,
|
||||||
|
"tokenizer.json", "application/json",
|
||||||
|
"tokenizer.model", "application/octet-stream",
|
||||||
|
) {
|
||||||
|
if !yield(match) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for match := range glob(fsys, "*.json", "") {
|
||||||
|
if !yield(match) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for match := range glob(fsys, "**/*.json", "") {
|
||||||
|
if !yield(match) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func get[T any](m map[string]any, key string) (t T) {
|
||||||
|
if v, ok := m[key].(T); ok {
|
||||||
|
t = v
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var deprecatedParameters = []string{
|
||||||
|
"penalize_newline",
|
||||||
|
"low_vram",
|
||||||
|
"f16_kv",
|
||||||
|
"logits_all",
|
||||||
|
"vocab_only",
|
||||||
|
"use_mlock",
|
||||||
|
"mirostat",
|
||||||
|
"mirostat_tau",
|
||||||
|
"mirostat_eta",
|
||||||
|
}
|
||||||
|
|
||||||
|
func createRequest(modelfile *parser.Modelfile, dir string) (*api.CreateRequest, error) {
|
||||||
|
m := make(map[string]any)
|
||||||
|
parameters := make(map[string]any)
|
||||||
|
var files, adapters []api.File
|
||||||
|
|
||||||
|
var g errgroup.Group
|
||||||
|
g.SetLimit(runtime.GOMAXPROCS(0))
|
||||||
|
for _, cmd := range modelfile.Commands {
|
||||||
|
switch cmd.Name {
|
||||||
|
case "model", "adapter":
|
||||||
|
path, err := expandPath(cmd.Args, dir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
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 !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 seq {
|
||||||
|
g.Go(func() error {
|
||||||
|
f, err := os.Open(filepath.Join(path, file))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
sha256sum := sha256.New()
|
||||||
|
if _, err := io.Copy(sha256sum, f); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
file := api.File{
|
||||||
|
Name: file,
|
||||||
|
Path: filepath.Join(path, file),
|
||||||
|
Digest: "sha256:" + hex.EncodeToString(sha256sum.Sum(nil)),
|
||||||
|
}
|
||||||
|
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
switch cmd.Name {
|
||||||
|
case "model":
|
||||||
|
files = append(files, file)
|
||||||
|
case "adapter":
|
||||||
|
adapters = append(adapters, file)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
case "template", "system", "renderer", "parser":
|
||||||
|
m[cmd.Name] = cmd.Args
|
||||||
|
case "license":
|
||||||
|
m[cmd.Name] = append(get[[]string](m, cmd.Name), cmd.Args)
|
||||||
|
case "message":
|
||||||
|
role, msg, found := strings.Cut(cmd.Args, ": ")
|
||||||
|
if !found {
|
||||||
|
return nil, fmt.Errorf("invalid message command: %s", cmd.Args)
|
||||||
|
}
|
||||||
|
|
||||||
|
m[cmd.Name] = append(get[[]api.Message](m, cmd.Name), api.Message{
|
||||||
|
Role: role,
|
||||||
|
Content: msg,
|
||||||
|
})
|
||||||
|
default:
|
||||||
|
if slices.Contains(deprecatedParameters, cmd.Name) {
|
||||||
|
slog.Warn("parameter is deprecated", "name", cmd.Name)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
ps, err := api.FormatParameters(map[string][]string{cmd.Name: {cmd.Args}})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range ps {
|
||||||
|
if ks, ok := parameters[k].([]string); ok {
|
||||||
|
parameters[k] = append(ks, v.([]string)...)
|
||||||
|
} else if vs, ok := v.([]string); ok {
|
||||||
|
parameters[k] = vs
|
||||||
|
} else {
|
||||||
|
parameters[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.Wait(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &api.CreateRequest{
|
||||||
|
From: get[string](m, "from"),
|
||||||
|
Files: files,
|
||||||
|
Adapters: adapters,
|
||||||
|
License: get[[]string](m, "license"),
|
||||||
|
Messages: get[[]api.Message](m, "message"),
|
||||||
|
Parameters: parameters,
|
||||||
|
Parser: get[string](m, "parser"),
|
||||||
|
Renderer: get[string](m, "renderer"),
|
||||||
|
System: get[string](m, "system"),
|
||||||
|
Template: get[string](m, "template"),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateHandler(cmd *cobra.Command, args []string) error {
|
||||||
|
p := progress.NewProgress(os.Stderr)
|
||||||
|
defer p.Stop()
|
||||||
|
|
||||||
|
var reader io.Reader
|
||||||
|
|
||||||
|
filename, err := getModelfileName(cmd)
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
if filename == "" {
|
||||||
|
reader = strings.NewReader("FROM .\n")
|
||||||
|
} else {
|
||||||
|
return errModelfileNotFound
|
||||||
|
}
|
||||||
|
} else if err != nil {
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
f, err := os.Open(filename)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
reader = f
|
||||||
|
defer f.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
modelfile, err := parser.ParseFile(reader)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
status := "gathering model components"
|
||||||
|
spinner := progress.NewSpinner(status)
|
||||||
|
p.Add(status, spinner)
|
||||||
|
|
||||||
|
req, err := createRequest(modelfile, filepath.Dir(filename))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
spinner.Stop()
|
||||||
|
|
||||||
|
req.Model = args[0]
|
||||||
|
quantize, _ := cmd.Flags().GetString("quantize")
|
||||||
|
if quantize != "" {
|
||||||
|
req.Quantize = quantize
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := api.ClientFromEnvironment()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var g errgroup.Group
|
||||||
|
g.SetLimit(runtime.GOMAXPROCS(0))
|
||||||
|
for _, file := range req.Files {
|
||||||
|
g.Go(func() error {
|
||||||
|
_, err := createBlob(cmd, client, file.Path, file.Digest, p)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.Wait(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
bars := make(map[string]*progress.Bar)
|
||||||
|
fn := func(resp api.ProgressResponse) error {
|
||||||
|
if resp.Digest != "" {
|
||||||
|
bar, ok := bars[resp.Digest]
|
||||||
|
if !ok {
|
||||||
|
msg := resp.Status
|
||||||
|
if msg == "" {
|
||||||
|
msg = fmt.Sprintf("pulling %s...", resp.Digest[7:19])
|
||||||
|
}
|
||||||
|
bar = progress.NewBar(msg, resp.Total, resp.Completed)
|
||||||
|
bars[resp.Digest] = bar
|
||||||
|
p.Add(resp.Digest, bar)
|
||||||
|
}
|
||||||
|
|
||||||
|
bar.Set(resp.Completed)
|
||||||
|
} else if status != resp.Status {
|
||||||
|
spinner.Stop()
|
||||||
|
|
||||||
|
status = resp.Status
|
||||||
|
spinner = progress.NewSpinner(status)
|
||||||
|
p.Add(status, spinner)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := client.Create(cmd.Context(), req, fn); err != nil {
|
||||||
|
if strings.Contains(err.Error(), "path or Modelfile are required") {
|
||||||
|
return fmt.Errorf("the ollama server must be updated to use `ollama create` with this client")
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createBlob(cmd *cobra.Command, client *api.Client, path string, digest string, p *progress.Progress) (string, error) {
|
||||||
|
realPath, err := filepath.EvalSymlinks(path)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
bin, err := os.Open(realPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer bin.Close()
|
||||||
|
|
||||||
|
// Get file info to retrieve the size
|
||||||
|
fileInfo, err := bin.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
fileSize := fileInfo.Size()
|
||||||
|
|
||||||
|
var pw progressWriter
|
||||||
|
status := fmt.Sprintf("copying file %s 0%%", digest)
|
||||||
|
spinner := progress.NewSpinner(status)
|
||||||
|
p.Add(status, spinner)
|
||||||
|
defer spinner.Stop()
|
||||||
|
|
||||||
|
done := make(chan struct{})
|
||||||
|
defer close(done)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
ticker := time.NewTicker(60 * time.Millisecond)
|
||||||
|
defer ticker.Stop()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
spinner.SetMessage(fmt.Sprintf("copying file %s %d%%", digest, int(100*pw.n.Load()/fileSize)))
|
||||||
|
case <-done:
|
||||||
|
spinner.SetMessage(fmt.Sprintf("copying file %s 100%%", digest))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err := client.CreateBlob(cmd.Context(), digest, io.TeeReader(bin, &pw)); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return digest, nil
|
||||||
|
}
|
||||||
258
cmd/create_test.go
Normal file
258
cmd/create_test.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -316,7 +316,7 @@ func generateInteractive(cmd *cobra.Command, opts runOptions) error {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
params := args[3:]
|
params := args[3:]
|
||||||
fp, err := api.FormatParams(map[string][]string{args[2]: params})
|
fp, err := api.FormatParameters(map[string][]string{args[2]: params})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Couldn't set parameter: %q\n", err)
|
fmt.Printf("Couldn't set parameter: %q\n", err)
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -1,123 +0,0 @@
|
|||||||
package parser
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"os/user"
|
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestExpandPath(t *testing.T) {
|
|
||||||
mockCurrentUser := func() (*user.User, error) {
|
|
||||||
return &user.User{
|
|
||||||
Username: "testuser",
|
|
||||||
HomeDir: func() string {
|
|
||||||
if os.PathSeparator == '\\' {
|
|
||||||
return filepath.FromSlash("D:/home/testuser")
|
|
||||||
}
|
|
||||||
return "/home/testuser"
|
|
||||||
}(),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
mockLookupUser := func(username string) (*user.User, error) {
|
|
||||||
fakeUsers := map[string]string{
|
|
||||||
"testuser": func() string {
|
|
||||||
if os.PathSeparator == '\\' {
|
|
||||||
return filepath.FromSlash("D:/home/testuser")
|
|
||||||
}
|
|
||||||
return "/home/testuser"
|
|
||||||
}(),
|
|
||||||
"anotheruser": func() string {
|
|
||||||
if os.PathSeparator == '\\' {
|
|
||||||
return filepath.FromSlash("D:/home/anotheruser")
|
|
||||||
}
|
|
||||||
return "/home/anotheruser"
|
|
||||||
}(),
|
|
||||||
}
|
|
||||||
|
|
||||||
if homeDir, ok := fakeUsers[username]; ok {
|
|
||||||
return &user.User{
|
|
||||||
Username: username,
|
|
||||||
HomeDir: homeDir,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
return nil, os.ErrNotExist
|
|
||||||
}
|
|
||||||
|
|
||||||
pwd, err := os.Getwd()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run("unix tests", func(t *testing.T) {
|
|
||||||
if runtime.GOOS == "windows" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
path string
|
|
||||||
relativeDir string
|
|
||||||
expected string
|
|
||||||
shouldErr bool
|
|
||||||
}{
|
|
||||||
{"~", "", "/home/testuser", false},
|
|
||||||
{"~/myfolder/myfile.txt", "", "/home/testuser/myfolder/myfile.txt", false},
|
|
||||||
{"~anotheruser/docs/file.txt", "", "/home/anotheruser/docs/file.txt", false},
|
|
||||||
{"~nonexistentuser/file.txt", "", "", true},
|
|
||||||
{"relative/path/to/file", "", filepath.Join(pwd, "relative/path/to/file"), false},
|
|
||||||
{"/absolute/path/to/file", "", "/absolute/path/to/file", false},
|
|
||||||
{"/absolute/path/to/file", "someotherdir/", "/absolute/path/to/file", false},
|
|
||||||
{".", pwd, pwd, false},
|
|
||||||
{".", "", pwd, false},
|
|
||||||
{"somefile", "somedir", filepath.Join(pwd, "somedir", "somefile"), false},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test := range tests {
|
|
||||||
result, err := expandPathImpl(test.path, test.relativeDir, mockCurrentUser, mockLookupUser)
|
|
||||||
if (err != nil) != test.shouldErr {
|
|
||||||
t.Errorf("expandPathImpl(%q) returned error: %v, expected error: %v", test.path, err != nil, test.shouldErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
if result != test.expected && !test.shouldErr {
|
|
||||||
t.Errorf("expandPathImpl(%q) = %q, want %q", test.path, result, test.expected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("windows tests", func(t *testing.T) {
|
|
||||||
if runtime.GOOS != "windows" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
path string
|
|
||||||
relativeDir string
|
|
||||||
expected string
|
|
||||||
shouldErr bool
|
|
||||||
}{
|
|
||||||
{"~", "", "D:\\home\\testuser", false},
|
|
||||||
{"~/myfolder/myfile.txt", "", "D:\\home\\testuser\\myfolder\\myfile.txt", false},
|
|
||||||
{"~anotheruser/docs/file.txt", "", "D:\\home\\anotheruser\\docs\\file.txt", false},
|
|
||||||
{"~nonexistentuser/file.txt", "", "", true},
|
|
||||||
{"relative\\path\\to\\file", "", filepath.Join(pwd, "relative\\path\\to\\file"), false},
|
|
||||||
{"D:\\absolute\\path\\to\\file", "", "D:\\absolute\\path\\to\\file", false},
|
|
||||||
{"D:\\absolute\\path\\to\\file", "someotherdir/", "D:\\absolute\\path\\to\\file", false},
|
|
||||||
{".", pwd, pwd, false},
|
|
||||||
{".", "", pwd, false},
|
|
||||||
{"somefile", "somedir", filepath.Join(pwd, "somedir", "somefile"), false},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test := range tests {
|
|
||||||
result, err := expandPathImpl(test.path, test.relativeDir, mockCurrentUser, mockLookupUser)
|
|
||||||
if (err != nil) != test.shouldErr {
|
|
||||||
t.Errorf("expandPathImpl(%q) returned error: %v, expected error: %v", test.path, err != nil, test.shouldErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
if result != test.expected && !test.shouldErr {
|
|
||||||
t.Errorf("expandPathImpl(%q) = %q, want %q", test.path, result, test.expected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
339
parser/parser.go
339
parser/parser.go
@@ -3,25 +3,14 @@ package parser
|
|||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
"crypto/sha256"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"os/user"
|
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
|
||||||
"slices"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
|
|
||||||
"golang.org/x/sync/errgroup"
|
|
||||||
"golang.org/x/text/encoding/unicode"
|
"golang.org/x/text/encoding/unicode"
|
||||||
"golang.org/x/text/transform"
|
"golang.org/x/text/transform"
|
||||||
|
|
||||||
"github.com/ollama/ollama/api"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var ErrModelNotFound = errors.New("no Modelfile or safetensors files found")
|
var ErrModelNotFound = errors.New("no Modelfile or safetensors files found")
|
||||||
@@ -39,281 +28,6 @@ func (f Modelfile) String() string {
|
|||||||
return sb.String()
|
return sb.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
var deprecatedParameters = []string{
|
|
||||||
"penalize_newline",
|
|
||||||
"low_vram",
|
|
||||||
"f16_kv",
|
|
||||||
"logits_all",
|
|
||||||
"vocab_only",
|
|
||||||
"use_mlock",
|
|
||||||
"mirostat",
|
|
||||||
"mirostat_tau",
|
|
||||||
"mirostat_eta",
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateRequest creates a new *api.CreateRequest from an existing Modelfile
|
|
||||||
func (f Modelfile) CreateRequest(relativeDir string) (*api.CreateRequest, error) {
|
|
||||||
req := &api.CreateRequest{}
|
|
||||||
|
|
||||||
var messages []api.Message
|
|
||||||
var licenses []string
|
|
||||||
params := make(map[string]any)
|
|
||||||
|
|
||||||
for _, c := range f.Commands {
|
|
||||||
switch c.Name {
|
|
||||||
case "model":
|
|
||||||
path, err := expandPath(c.Args, relativeDir)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
digestMap, err := fileDigestMap(path)
|
|
||||||
if errors.Is(err, os.ErrNotExist) {
|
|
||||||
req.From = c.Args
|
|
||||||
continue
|
|
||||||
} else if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if req.Files == nil {
|
|
||||||
req.Files = digestMap
|
|
||||||
} else {
|
|
||||||
for k, v := range digestMap {
|
|
||||||
req.Files[k] = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case "adapter":
|
|
||||||
path, err := expandPath(c.Args, relativeDir)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
digestMap, err := fileDigestMap(path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
req.Adapters = digestMap
|
|
||||||
case "template":
|
|
||||||
req.Template = c.Args
|
|
||||||
case "system":
|
|
||||||
req.System = c.Args
|
|
||||||
case "license":
|
|
||||||
licenses = append(licenses, c.Args)
|
|
||||||
case "renderer":
|
|
||||||
req.Renderer = c.Args
|
|
||||||
case "parser":
|
|
||||||
req.Parser = c.Args
|
|
||||||
case "message":
|
|
||||||
role, msg, _ := strings.Cut(c.Args, ": ")
|
|
||||||
messages = append(messages, api.Message{Role: role, Content: msg})
|
|
||||||
default:
|
|
||||||
if slices.Contains(deprecatedParameters, c.Name) {
|
|
||||||
fmt.Printf("warning: parameter %s is deprecated\n", c.Name)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
ps, err := api.FormatParams(map[string][]string{c.Name: {c.Args}})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for k, v := range ps {
|
|
||||||
if ks, ok := params[k].([]string); ok {
|
|
||||||
params[k] = append(ks, v.([]string)...)
|
|
||||||
} else if vs, ok := v.([]string); ok {
|
|
||||||
params[k] = vs
|
|
||||||
} else {
|
|
||||||
params[k] = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(params) > 0 {
|
|
||||||
req.Parameters = params
|
|
||||||
}
|
|
||||||
if len(messages) > 0 {
|
|
||||||
req.Messages = messages
|
|
||||||
}
|
|
||||||
if len(licenses) > 0 {
|
|
||||||
req.License = licenses
|
|
||||||
}
|
|
||||||
|
|
||||||
return req, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func fileDigestMap(path string) (map[string]string, error) {
|
|
||||||
fl := make(map[string]string)
|
|
||||||
|
|
||||||
fi, err := os.Stat(path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var files []string
|
|
||||||
if fi.IsDir() {
|
|
||||||
fs, err := filesForModel(path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, f := range fs {
|
|
||||||
f, err := filepath.EvalSymlinks(f)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
rel, err := filepath.Rel(path, f)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !filepath.IsLocal(rel) {
|
|
||||||
return nil, fmt.Errorf("insecure path: %s", rel)
|
|
||||||
}
|
|
||||||
|
|
||||||
files = append(files, f)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
files = []string{path}
|
|
||||||
}
|
|
||||||
|
|
||||||
var mu sync.Mutex
|
|
||||||
var g errgroup.Group
|
|
||||||
g.SetLimit(max(runtime.GOMAXPROCS(0)-1, 1))
|
|
||||||
for _, f := range files {
|
|
||||||
g.Go(func() error {
|
|
||||||
digest, err := digestForFile(f)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
mu.Lock()
|
|
||||||
defer mu.Unlock()
|
|
||||||
fl[f] = digest
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := g.Wait(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return fl, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func digestForFile(filename string) (string, error) {
|
|
||||||
filepath, err := filepath.EvalSymlinks(filename)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
bin, err := os.Open(filepath)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
defer bin.Close()
|
|
||||||
|
|
||||||
hash := sha256.New()
|
|
||||||
if _, err := io.Copy(hash, bin); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("sha256:%x", hash.Sum(nil)), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func filesForModel(path string) ([]string, error) {
|
|
||||||
detectContentType := func(path string) (string, error) {
|
|
||||||
f, err := os.Open(path)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
|
|
||||||
var b bytes.Buffer
|
|
||||||
b.Grow(512)
|
|
||||||
|
|
||||||
if _, err := io.CopyN(&b, f, 512); err != nil && !errors.Is(err, io.EOF) {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
contentType, _, _ := strings.Cut(http.DetectContentType(b.Bytes()), ";")
|
|
||||||
return contentType, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
glob := func(pattern, contentType string) ([]string, error) {
|
|
||||||
matches, err := filepath.Glob(pattern)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, match := range matches {
|
|
||||||
if ct, err := detectContentType(match); err != nil {
|
|
||||||
return nil, err
|
|
||||||
} else if len(contentType) > 0 && ct != contentType {
|
|
||||||
return nil, fmt.Errorf("invalid content type: expected %s for %s", ct, match)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return matches, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var files []string
|
|
||||||
// some safetensors files do not properly match "application/octet-stream", so skip checking their contentType
|
|
||||||
if st, _ := glob(filepath.Join(path, "*.safetensors"), ""); len(st) > 0 {
|
|
||||||
// safetensors files might be unresolved git lfs references; skip if they are
|
|
||||||
// covers model-x-of-y.safetensors, model.fp32-x-of-y.safetensors, model.safetensors
|
|
||||||
files = append(files, st...)
|
|
||||||
} else if pt, _ := glob(filepath.Join(path, "pytorch_model*.bin"), "application/zip"); len(pt) > 0 {
|
|
||||||
// pytorch files might also be unresolved git lfs references; skip if they are
|
|
||||||
// covers pytorch_model-x-of-y.bin, pytorch_model.fp32-x-of-y.bin, pytorch_model.bin
|
|
||||||
files = append(files, pt...)
|
|
||||||
} else if pt, _ := glob(filepath.Join(path, "consolidated*.pth"), "application/zip"); len(pt) > 0 {
|
|
||||||
// pytorch files might also be unresolved git lfs references; skip if they are
|
|
||||||
// covers consolidated.x.pth, consolidated.pth
|
|
||||||
files = append(files, pt...)
|
|
||||||
} else if gg, _ := glob(filepath.Join(path, "*.gguf"), "application/octet-stream"); len(gg) > 0 {
|
|
||||||
// covers gguf files ending in .gguf
|
|
||||||
files = append(files, gg...)
|
|
||||||
} else if gg, _ := glob(filepath.Join(path, "*.bin"), "application/octet-stream"); len(gg) > 0 {
|
|
||||||
// covers gguf files ending in .bin
|
|
||||||
files = append(files, gg...)
|
|
||||||
} else {
|
|
||||||
return nil, ErrModelNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
// add configuration files, json files are detected as text/plain
|
|
||||||
js, err := glob(filepath.Join(path, "*.json"), "text/plain")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
files = append(files, js...)
|
|
||||||
|
|
||||||
// bert models require a nested config.json
|
|
||||||
// TODO(mxyng): merge this with the glob above
|
|
||||||
js, err = glob(filepath.Join(path, "**/*.json"), "text/plain")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
files = append(files, js...)
|
|
||||||
|
|
||||||
// only include tokenizer.model is tokenizer.json is not present
|
|
||||||
if !slices.ContainsFunc(files, func(s string) bool {
|
|
||||||
return slices.Contains(strings.Split(s, string(os.PathSeparator)), "tokenizer.json")
|
|
||||||
}) {
|
|
||||||
if tks, _ := glob(filepath.Join(path, "tokenizer.model"), "application/octet-stream"); len(tks) > 0 {
|
|
||||||
// add tokenizer.model if it exists, tokenizer.json is automatically picked up by the previous glob
|
|
||||||
// tokenizer.model might be a unresolved git lfs reference; error if it is
|
|
||||||
files = append(files, tks...)
|
|
||||||
} else if tks, _ := glob(filepath.Join(path, "**/tokenizer.model"), "text/plain"); len(tks) > 0 {
|
|
||||||
// some times tokenizer.model is in a subdirectory (e.g. meta-llama/Meta-Llama-3-8B)
|
|
||||||
files = append(files, tks...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return files, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type Command struct {
|
type Command struct {
|
||||||
Name string
|
Name string
|
||||||
Args string
|
Args string
|
||||||
@@ -340,7 +54,7 @@ type state int
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
stateNil state = iota
|
stateNil state = iota
|
||||||
stateName
|
stateKey
|
||||||
stateValue
|
stateValue
|
||||||
stateParameter
|
stateParameter
|
||||||
stateMessage
|
stateMessage
|
||||||
@@ -368,7 +82,7 @@ func (e *ParserError) Error() string {
|
|||||||
func ParseFile(r io.Reader) (*Modelfile, error) {
|
func ParseFile(r io.Reader) (*Modelfile, error) {
|
||||||
var cmd Command
|
var cmd Command
|
||||||
var curr state
|
var curr state
|
||||||
var currLine int = 1
|
currLine := 1
|
||||||
var b bytes.Buffer
|
var b bytes.Buffer
|
||||||
var role string
|
var role string
|
||||||
|
|
||||||
@@ -402,7 +116,7 @@ func ParseFile(r io.Reader) (*Modelfile, error) {
|
|||||||
// process the state transition, some transitions need to be intercepted and redirected
|
// process the state transition, some transitions need to be intercepted and redirected
|
||||||
if next != curr {
|
if next != curr {
|
||||||
switch curr {
|
switch curr {
|
||||||
case stateName:
|
case stateKey:
|
||||||
if !isValidCommand(b.String()) {
|
if !isValidCommand(b.String()) {
|
||||||
return nil, &ParserError{
|
return nil, &ParserError{
|
||||||
LineNumber: currLine,
|
LineNumber: currLine,
|
||||||
@@ -505,12 +219,12 @@ func parseRuneForState(r rune, cs state) (state, rune, error) {
|
|||||||
case isSpace(r), isNewline(r):
|
case isSpace(r), isNewline(r):
|
||||||
return stateNil, 0, nil
|
return stateNil, 0, nil
|
||||||
default:
|
default:
|
||||||
return stateName, r, nil
|
return stateKey, r, nil
|
||||||
}
|
}
|
||||||
case stateName:
|
case stateKey:
|
||||||
switch {
|
switch {
|
||||||
case isAlpha(r):
|
case isAlpha(r):
|
||||||
return stateName, r, nil
|
return stateKey, r, nil
|
||||||
case isSpace(r):
|
case isSpace(r):
|
||||||
return stateValue, 0, nil
|
return stateValue, 0, nil
|
||||||
default:
|
default:
|
||||||
@@ -616,44 +330,3 @@ func isValidCommand(cmd string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func expandPathImpl(path, relativeDir string, currentUserFunc func() (*user.User, error), lookupUserFunc func(string) (*user.User, error)) (string, error) {
|
|
||||||
if filepath.IsAbs(path) || strings.HasPrefix(path, "\\") || strings.HasPrefix(path, "/") {
|
|
||||||
return filepath.Abs(path)
|
|
||||||
} else if strings.HasPrefix(path, "~") {
|
|
||||||
var homeDir string
|
|
||||||
|
|
||||||
if path == "~" || strings.HasPrefix(path, "~/") {
|
|
||||||
// Current user's home directory
|
|
||||||
currentUser, err := currentUserFunc()
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("failed to get current user: %w", err)
|
|
||||||
}
|
|
||||||
homeDir = currentUser.HomeDir
|
|
||||||
path = strings.TrimPrefix(path, "~")
|
|
||||||
} else {
|
|
||||||
// Specific user's home directory
|
|
||||||
parts := strings.SplitN(path[1:], "/", 2)
|
|
||||||
userInfo, err := lookupUserFunc(parts[0])
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("failed to find user '%s': %w", parts[0], err)
|
|
||||||
}
|
|
||||||
homeDir = userInfo.HomeDir
|
|
||||||
if len(parts) > 1 {
|
|
||||||
path = "/" + parts[1]
|
|
||||||
} else {
|
|
||||||
path = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
path = filepath.Join(homeDir, path)
|
|
||||||
} else {
|
|
||||||
path = filepath.Join(relativeDir, path)
|
|
||||||
}
|
|
||||||
|
|
||||||
return filepath.Abs(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
func expandPath(path, relativeDir string) (string, error) {
|
|
||||||
return expandPathImpl(path, relativeDir, user.Current, user.Lookup)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,24 +2,18 @@ package parser
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"crypto/sha256"
|
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"unicode/utf16"
|
"unicode/utf16"
|
||||||
|
|
||||||
"github.com/google/go-cmp/cmp"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"golang.org/x/text/encoding"
|
"golang.org/x/text/encoding"
|
||||||
"golang.org/x/text/encoding/unicode"
|
"golang.org/x/text/encoding/unicode"
|
||||||
|
|
||||||
"github.com/ollama/ollama/api"
|
|
||||||
"github.com/ollama/ollama/fs/ggml"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestParseFileFile(t *testing.T) {
|
func TestParseFileFile(t *testing.T) {
|
||||||
@@ -699,155 +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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -62,8 +62,8 @@ func (s *Server) CreateHandler(c *gin.Context) {
|
|||||||
config.Renderer = r.Renderer
|
config.Renderer = r.Renderer
|
||||||
config.Parser = r.Parser
|
config.Parser = r.Parser
|
||||||
|
|
||||||
for v := range r.Files {
|
for _, v := range r.Files {
|
||||||
if !fs.ValidPath(v) {
|
if !fs.ValidPath(v.Name) {
|
||||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": errFilePath.Error()})
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": errFilePath.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -276,7 +276,7 @@ func remoteURL(raw string) (string, error) {
|
|||||||
return u.String(), nil
|
return u.String(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func convertModelFromFiles(files map[string]string, baseLayers []*layerGGML, isAdapter bool, fn func(resp api.ProgressResponse)) ([]*layerGGML, error) {
|
func convertModelFromFiles(files api.Files, baseLayers []*layerGGML, isAdapter bool, fn func(resp api.ProgressResponse)) ([]*layerGGML, error) {
|
||||||
switch detectModelTypeFromFiles(files) {
|
switch detectModelTypeFromFiles(files) {
|
||||||
case "safetensors":
|
case "safetensors":
|
||||||
layers, err := convertFromSafetensors(files, baseLayers, isAdapter, fn)
|
layers, err := convertFromSafetensors(files, baseLayers, isAdapter, fn)
|
||||||
@@ -295,7 +295,7 @@ func convertModelFromFiles(files map[string]string, baseLayers []*layerGGML, isA
|
|||||||
var digest string
|
var digest string
|
||||||
var allLayers []*layerGGML
|
var allLayers []*layerGGML
|
||||||
for _, v := range files {
|
for _, v := range files {
|
||||||
digest = v
|
digest = v.Digest
|
||||||
layers, err := ggufLayers(digest, fn)
|
layers, err := ggufLayers(digest, fn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -308,15 +308,15 @@ func convertModelFromFiles(files map[string]string, baseLayers []*layerGGML, isA
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func detectModelTypeFromFiles(files map[string]string) string {
|
func detectModelTypeFromFiles(files api.Files) string {
|
||||||
for fn := range files {
|
for _, fn := range files {
|
||||||
if strings.HasSuffix(fn, ".safetensors") {
|
if strings.HasSuffix(fn.Name, ".safetensors") {
|
||||||
return "safetensors"
|
return "safetensors"
|
||||||
} else if strings.HasSuffix(fn, ".gguf") {
|
} else if strings.HasSuffix(fn.Name, ".gguf") {
|
||||||
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 := GetBlobsPath(fn.Digest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("error getting blobs path", "file", fn)
|
slog.Error("error getting blobs path", "file", fn)
|
||||||
return ""
|
return ""
|
||||||
@@ -346,7 +346,7 @@ func detectModelTypeFromFiles(files map[string]string) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func convertFromSafetensors(files map[string]string, baseLayers []*layerGGML, isAdapter bool, fn func(resp api.ProgressResponse)) ([]*layerGGML, error) {
|
func convertFromSafetensors(files api.Files, baseLayers []*layerGGML, isAdapter bool, fn func(resp api.ProgressResponse)) ([]*layerGGML, error) {
|
||||||
tmpDir, err := os.MkdirTemp(envconfig.Models(), "ollama-safetensors")
|
tmpDir, err := os.MkdirTemp(envconfig.Models(), "ollama-safetensors")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -359,20 +359,20 @@ func convertFromSafetensors(files map[string]string, baseLayers []*layerGGML, is
|
|||||||
}
|
}
|
||||||
defer root.Close()
|
defer root.Close()
|
||||||
|
|
||||||
for fp, digest := range files {
|
for _, fn := range files {
|
||||||
if !fs.ValidPath(fp) {
|
if !fs.ValidPath(fn.Name) {
|
||||||
return nil, fmt.Errorf("%w: %s", errFilePath, fp)
|
return nil, fmt.Errorf("%w: %s", errFilePath, fn)
|
||||||
}
|
}
|
||||||
if _, err := root.Stat(fp); err != nil && !errors.Is(err, fs.ErrNotExist) {
|
if _, err := root.Stat(fn.Name); err != nil && !errors.Is(err, fs.ErrNotExist) {
|
||||||
// Path is likely outside the root
|
// Path is likely outside the root
|
||||||
return nil, fmt.Errorf("%w: %s: %s", errFilePath, err, fp)
|
return nil, fmt.Errorf("%w: %s: %s", errFilePath, err, fn)
|
||||||
}
|
}
|
||||||
|
|
||||||
blobPath, err := GetBlobsPath(digest)
|
blobPath, err := GetBlobsPath(fn.Digest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if err := createLink(blobPath, filepath.Join(tmpDir, fp)); err != nil {
|
if err := createLink(blobPath, filepath.Join(tmpDir, fn.Name)); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,10 +88,10 @@ func TestConvertFromSafetensors(t *testing.T) {
|
|||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
// Create the minimum required file map for convertFromSafetensors
|
// Create the minimum required file map for convertFromSafetensors
|
||||||
files := map[string]string{
|
files := []api.File{
|
||||||
tt.filePath: model,
|
{Name: tt.filePath, Digest: model},
|
||||||
"config.json": config,
|
{Name: "config.json", Digest: config},
|
||||||
"tokenizer.json": tokenizer,
|
{Name: "tokenizer.json", Digest: tokenizer},
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := convertFromSafetensors(files, nil, false, func(resp api.ProgressResponse) {})
|
_, err := convertFromSafetensors(files, nil, false, func(resp api.ProgressResponse) {})
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ func TestCreateFromBin(t *testing.T) {
|
|||||||
|
|
||||||
w := createRequest(t, s.CreateHandler, api.CreateRequest{
|
w := createRequest(t, s.CreateHandler, api.CreateRequest{
|
||||||
Name: "test",
|
Name: "test",
|
||||||
Files: map[string]string{"test.gguf": digest},
|
Files: []api.File{{Name: "test.gguf", Digest: digest}},
|
||||||
Stream: &stream,
|
Stream: &stream,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -149,7 +149,7 @@ func TestCreateFromModel(t *testing.T) {
|
|||||||
|
|
||||||
w := createRequest(t, s.CreateHandler, api.CreateRequest{
|
w := createRequest(t, s.CreateHandler, api.CreateRequest{
|
||||||
Name: "test",
|
Name: "test",
|
||||||
Files: map[string]string{"test.gguf": digest},
|
Files: []api.File{{Name: "test.gguf", Digest: digest}},
|
||||||
Stream: &stream,
|
Stream: &stream,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -192,7 +192,7 @@ func TestCreateRemovesLayers(t *testing.T) {
|
|||||||
_, digest := createBinFile(t, nil, nil)
|
_, digest := createBinFile(t, nil, nil)
|
||||||
w := createRequest(t, s.CreateHandler, api.CreateRequest{
|
w := createRequest(t, s.CreateHandler, api.CreateRequest{
|
||||||
Name: "test",
|
Name: "test",
|
||||||
Files: map[string]string{"test.gguf": digest},
|
Files: []api.File{{Name: "test.gguf", Digest: digest}},
|
||||||
Template: "{{ .Prompt }}",
|
Template: "{{ .Prompt }}",
|
||||||
Stream: &stream,
|
Stream: &stream,
|
||||||
})
|
})
|
||||||
@@ -213,7 +213,7 @@ func TestCreateRemovesLayers(t *testing.T) {
|
|||||||
|
|
||||||
w = createRequest(t, s.CreateHandler, api.CreateRequest{
|
w = createRequest(t, s.CreateHandler, api.CreateRequest{
|
||||||
Name: "test",
|
Name: "test",
|
||||||
Files: map[string]string{"test.gguf": digest},
|
Files: []api.File{{Name: "test.gguf", Digest: digest}},
|
||||||
Template: "{{ .System }} {{ .Prompt }}",
|
Template: "{{ .System }} {{ .Prompt }}",
|
||||||
Stream: &stream,
|
Stream: &stream,
|
||||||
})
|
})
|
||||||
@@ -243,7 +243,7 @@ func TestCreateUnsetsSystem(t *testing.T) {
|
|||||||
_, digest := createBinFile(t, nil, nil)
|
_, digest := createBinFile(t, nil, nil)
|
||||||
w := createRequest(t, s.CreateHandler, api.CreateRequest{
|
w := createRequest(t, s.CreateHandler, api.CreateRequest{
|
||||||
Name: "test",
|
Name: "test",
|
||||||
Files: map[string]string{"test.gguf": digest},
|
Files: []api.File{{Name: "test.gguf", Digest: digest}},
|
||||||
System: "Say hi!",
|
System: "Say hi!",
|
||||||
Stream: &stream,
|
Stream: &stream,
|
||||||
})
|
})
|
||||||
@@ -264,7 +264,7 @@ func TestCreateUnsetsSystem(t *testing.T) {
|
|||||||
|
|
||||||
w = createRequest(t, s.CreateHandler, api.CreateRequest{
|
w = createRequest(t, s.CreateHandler, api.CreateRequest{
|
||||||
Name: "test",
|
Name: "test",
|
||||||
Files: map[string]string{"test.gguf": digest},
|
Files: []api.File{{Name: "test.gguf", Digest: digest}},
|
||||||
System: "",
|
System: "",
|
||||||
Stream: &stream,
|
Stream: &stream,
|
||||||
})
|
})
|
||||||
@@ -293,7 +293,7 @@ func TestCreateMergeParameters(t *testing.T) {
|
|||||||
_, digest := createBinFile(t, nil, nil)
|
_, digest := createBinFile(t, nil, nil)
|
||||||
w := createRequest(t, s.CreateHandler, api.CreateRequest{
|
w := createRequest(t, s.CreateHandler, api.CreateRequest{
|
||||||
Name: "test",
|
Name: "test",
|
||||||
Files: map[string]string{"test.gguf": digest},
|
Files: []api.File{{Name: "test.gguf", Digest: digest}},
|
||||||
Parameters: map[string]any{
|
Parameters: map[string]any{
|
||||||
"temperature": 1,
|
"temperature": 1,
|
||||||
"top_k": 10,
|
"top_k": 10,
|
||||||
@@ -428,7 +428,7 @@ func TestCreateReplacesMessages(t *testing.T) {
|
|||||||
_, digest := createBinFile(t, nil, nil)
|
_, digest := createBinFile(t, nil, nil)
|
||||||
w := createRequest(t, s.CreateHandler, api.CreateRequest{
|
w := createRequest(t, s.CreateHandler, api.CreateRequest{
|
||||||
Name: "test",
|
Name: "test",
|
||||||
Files: map[string]string{"test.gguf": digest},
|
Files: []api.File{{Name: "test.gguf", Digest: digest}},
|
||||||
Messages: []api.Message{
|
Messages: []api.Message{
|
||||||
{
|
{
|
||||||
Role: "assistant",
|
Role: "assistant",
|
||||||
@@ -535,7 +535,7 @@ func TestCreateTemplateSystem(t *testing.T) {
|
|||||||
_, digest := createBinFile(t, nil, nil)
|
_, digest := createBinFile(t, nil, nil)
|
||||||
w := createRequest(t, s.CreateHandler, api.CreateRequest{
|
w := createRequest(t, s.CreateHandler, api.CreateRequest{
|
||||||
Name: "test",
|
Name: "test",
|
||||||
Files: map[string]string{"test.gguf": digest},
|
Files: []api.File{{Name: "test.gguf", Digest: digest}},
|
||||||
Template: "{{ .System }} {{ .Prompt }}",
|
Template: "{{ .System }} {{ .Prompt }}",
|
||||||
System: "Say bye!",
|
System: "Say bye!",
|
||||||
Stream: &stream,
|
Stream: &stream,
|
||||||
@@ -578,7 +578,7 @@ func TestCreateTemplateSystem(t *testing.T) {
|
|||||||
_, digest := createBinFile(t, nil, nil)
|
_, digest := createBinFile(t, nil, nil)
|
||||||
w := createRequest(t, s.CreateHandler, api.CreateRequest{
|
w := createRequest(t, s.CreateHandler, api.CreateRequest{
|
||||||
Name: "test",
|
Name: "test",
|
||||||
Files: map[string]string{"test.gguf": digest},
|
Files: []api.File{{Name: "test.gguf", Digest: digest}},
|
||||||
Template: "{{ .Prompt",
|
Template: "{{ .Prompt",
|
||||||
Stream: &stream,
|
Stream: &stream,
|
||||||
})
|
})
|
||||||
@@ -592,7 +592,7 @@ func TestCreateTemplateSystem(t *testing.T) {
|
|||||||
_, digest := createBinFile(t, nil, nil)
|
_, digest := createBinFile(t, nil, nil)
|
||||||
w := createRequest(t, s.CreateHandler, api.CreateRequest{
|
w := createRequest(t, s.CreateHandler, api.CreateRequest{
|
||||||
Name: "test",
|
Name: "test",
|
||||||
Files: map[string]string{"test.gguf": digest},
|
Files: []api.File{{Name: "test.gguf", Digest: digest}},
|
||||||
Template: "{{ if .Prompt }}",
|
Template: "{{ if .Prompt }}",
|
||||||
Stream: &stream,
|
Stream: &stream,
|
||||||
})
|
})
|
||||||
@@ -606,7 +606,7 @@ func TestCreateTemplateSystem(t *testing.T) {
|
|||||||
_, digest := createBinFile(t, nil, nil)
|
_, digest := createBinFile(t, nil, nil)
|
||||||
w := createRequest(t, s.CreateHandler, api.CreateRequest{
|
w := createRequest(t, s.CreateHandler, api.CreateRequest{
|
||||||
Name: "test",
|
Name: "test",
|
||||||
Files: map[string]string{"test.gguf": digest},
|
Files: []api.File{{Name: "test.gguf", Digest: digest}},
|
||||||
Template: "{{ Prompt }}",
|
Template: "{{ Prompt }}",
|
||||||
Stream: &stream,
|
Stream: &stream,
|
||||||
})
|
})
|
||||||
@@ -699,7 +699,7 @@ func TestCreateLicenses(t *testing.T) {
|
|||||||
_, digest := createBinFile(t, nil, nil)
|
_, digest := createBinFile(t, nil, nil)
|
||||||
w := createRequest(t, s.CreateHandler, api.CreateRequest{
|
w := createRequest(t, s.CreateHandler, api.CreateRequest{
|
||||||
Name: "test",
|
Name: "test",
|
||||||
Files: map[string]string{"test.gguf": digest},
|
Files: []api.File{{Name: "test.gguf", Digest: digest}},
|
||||||
License: []string{"MIT", "Apache-2.0"},
|
License: []string{"MIT", "Apache-2.0"},
|
||||||
Stream: &stream,
|
Stream: &stream,
|
||||||
})
|
})
|
||||||
@@ -751,7 +751,7 @@ func TestCreateDetectTemplate(t *testing.T) {
|
|||||||
}, nil)
|
}, nil)
|
||||||
w := createRequest(t, s.CreateHandler, api.CreateRequest{
|
w := createRequest(t, s.CreateHandler, api.CreateRequest{
|
||||||
Name: "test",
|
Name: "test",
|
||||||
Files: map[string]string{"test.gguf": digest},
|
Files: []api.File{{Name: "test.gguf", Digest: digest}},
|
||||||
Stream: &stream,
|
Stream: &stream,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -771,7 +771,7 @@ func TestCreateDetectTemplate(t *testing.T) {
|
|||||||
_, digest := createBinFile(t, nil, nil)
|
_, digest := createBinFile(t, nil, nil)
|
||||||
w := createRequest(t, s.CreateHandler, api.CreateRequest{
|
w := createRequest(t, s.CreateHandler, api.CreateRequest{
|
||||||
Name: "test",
|
Name: "test",
|
||||||
Files: map[string]string{"test.gguf": digest},
|
Files: []api.File{{Name: "test.gguf", Digest: digest}},
|
||||||
Stream: &stream,
|
Stream: &stream,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -789,9 +789,7 @@ func TestCreateDetectTemplate(t *testing.T) {
|
|||||||
func TestDetectModelTypeFromFiles(t *testing.T) {
|
func TestDetectModelTypeFromFiles(t *testing.T) {
|
||||||
t.Run("gguf file", func(t *testing.T) {
|
t.Run("gguf file", func(t *testing.T) {
|
||||||
_, digest := createBinFile(t, nil, nil)
|
_, digest := createBinFile(t, nil, nil)
|
||||||
files := map[string]string{
|
files := []api.File{{Name: "model.gguf", Digest: digest}}
|
||||||
"model.gguf": digest,
|
|
||||||
}
|
|
||||||
|
|
||||||
modelType := detectModelTypeFromFiles(files)
|
modelType := detectModelTypeFromFiles(files)
|
||||||
if modelType != "gguf" {
|
if modelType != "gguf" {
|
||||||
@@ -801,8 +799,8 @@ func TestDetectModelTypeFromFiles(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("gguf file w/o extension", func(t *testing.T) {
|
t.Run("gguf file w/o extension", func(t *testing.T) {
|
||||||
_, digest := createBinFile(t, nil, nil)
|
_, digest := createBinFile(t, nil, nil)
|
||||||
files := map[string]string{
|
files := []api.File{
|
||||||
fmt.Sprintf("%x", digest): digest,
|
{Name: fmt.Sprintf("%x", digest), Digest: digest},
|
||||||
}
|
}
|
||||||
|
|
||||||
modelType := detectModelTypeFromFiles(files)
|
modelType := detectModelTypeFromFiles(files)
|
||||||
@@ -812,8 +810,8 @@ func TestDetectModelTypeFromFiles(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("safetensors file", func(t *testing.T) {
|
t.Run("safetensors file", func(t *testing.T) {
|
||||||
files := map[string]string{
|
files := []api.File{
|
||||||
"model.safetensors": "sha256:abc123",
|
{Name: "model.safetensors", Digest: "sha256:abc123"},
|
||||||
}
|
}
|
||||||
|
|
||||||
modelType := detectModelTypeFromFiles(files)
|
modelType := detectModelTypeFromFiles(files)
|
||||||
@@ -842,8 +840,8 @@ func TestDetectModelTypeFromFiles(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
files := map[string]string{
|
files := []api.File{
|
||||||
"model.bin": digest,
|
{Name: "model.bin", Digest: digest},
|
||||||
}
|
}
|
||||||
|
|
||||||
modelType := detectModelTypeFromFiles(files)
|
modelType := detectModelTypeFromFiles(files)
|
||||||
@@ -872,8 +870,8 @@ func TestDetectModelTypeFromFiles(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
files := map[string]string{
|
files := []api.File{
|
||||||
"noext": digest,
|
{Name: "noext", Digest: digest},
|
||||||
}
|
}
|
||||||
|
|
||||||
modelType := detectModelTypeFromFiles(files)
|
modelType := detectModelTypeFromFiles(files)
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ func TestGenerateDebugRenderOnly(t *testing.T) {
|
|||||||
|
|
||||||
w := createRequest(t, s.CreateHandler, api.CreateRequest{
|
w := createRequest(t, s.CreateHandler, api.CreateRequest{
|
||||||
Model: "test-model",
|
Model: "test-model",
|
||||||
Files: map[string]string{"file.gguf": digest},
|
Files: []api.File{{Name: "file.gguf", Digest: digest}},
|
||||||
Template: "{{ .Prompt }}",
|
Template: "{{ .Prompt }}",
|
||||||
Stream: &stream,
|
Stream: &stream,
|
||||||
})
|
})
|
||||||
@@ -273,7 +273,7 @@ func TestChatDebugRenderOnly(t *testing.T) {
|
|||||||
|
|
||||||
w := createRequest(t, s.CreateHandler, api.CreateRequest{
|
w := createRequest(t, s.CreateHandler, api.CreateRequest{
|
||||||
Model: "test-model",
|
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 }}",
|
Template: "{{ if .Tools }}{{ .Tools }}{{ end }}{{ range .Messages }}{{ .Role }}: {{ .Content }}\n{{ end }}",
|
||||||
Stream: &stream,
|
Stream: &stream,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ func TestDelete(t *testing.T) {
|
|||||||
_, digest := createBinFile(t, nil, nil)
|
_, digest := createBinFile(t, nil, nil)
|
||||||
w := createRequest(t, s.CreateHandler, api.CreateRequest{
|
w := createRequest(t, s.CreateHandler, api.CreateRequest{
|
||||||
Name: "test",
|
Name: "test",
|
||||||
Files: map[string]string{"test.gguf": digest},
|
Files: []api.File{{Name: "test.gguf", Digest: digest}},
|
||||||
})
|
})
|
||||||
|
|
||||||
if w.Code != http.StatusOK {
|
if w.Code != http.StatusOK {
|
||||||
@@ -33,7 +33,7 @@ func TestDelete(t *testing.T) {
|
|||||||
|
|
||||||
w = createRequest(t, s.CreateHandler, api.CreateRequest{
|
w = createRequest(t, s.CreateHandler, api.CreateRequest{
|
||||||
Name: "test2",
|
Name: "test2",
|
||||||
Files: map[string]string{"test.gguf": digest},
|
Files: []api.File{{Name: "test.gguf", Digest: digest}},
|
||||||
Template: "{{ .System }} {{ .Prompt }}",
|
Template: "{{ .System }} {{ .Prompt }}",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ func TestGenerateChat(t *testing.T) {
|
|||||||
|
|
||||||
w := createRequest(t, s.CreateHandler, api.CreateRequest{
|
w := createRequest(t, s.CreateHandler, api.CreateRequest{
|
||||||
Model: "test",
|
Model: "test",
|
||||||
Files: map[string]string{"file.gguf": digest},
|
Files: []api.File{{Name: "file.gguf", Digest: digest}},
|
||||||
Template: `
|
Template: `
|
||||||
{{- if .Tools }}
|
{{- if .Tools }}
|
||||||
{{ .Tools }}
|
{{ .Tools }}
|
||||||
@@ -181,7 +181,7 @@ func TestGenerateChat(t *testing.T) {
|
|||||||
}, []*ggml.Tensor{})
|
}, []*ggml.Tensor{})
|
||||||
w := createRequest(t, s.CreateHandler, api.CreateRequest{
|
w := createRequest(t, s.CreateHandler, api.CreateRequest{
|
||||||
Model: "bert",
|
Model: "bert",
|
||||||
Files: map[string]string{"bert.gguf": digest},
|
Files: []api.File{{Name: "bert.gguf", Digest: digest}},
|
||||||
Stream: &stream,
|
Stream: &stream,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -660,7 +660,7 @@ func TestGenerate(t *testing.T) {
|
|||||||
|
|
||||||
w := createRequest(t, s.CreateHandler, api.CreateRequest{
|
w := createRequest(t, s.CreateHandler, api.CreateRequest{
|
||||||
Model: "test",
|
Model: "test",
|
||||||
Files: map[string]string{"file.gguf": digest},
|
Files: []api.File{{Name: "file.gguf", Digest: digest}},
|
||||||
Template: `
|
Template: `
|
||||||
{{- if .System }}System: {{ .System }} {{ end }}
|
{{- if .System }}System: {{ .System }} {{ end }}
|
||||||
{{- if .Prompt }}User: {{ .Prompt }} {{ end }}
|
{{- if .Prompt }}User: {{ .Prompt }} {{ end }}
|
||||||
@@ -703,7 +703,7 @@ func TestGenerate(t *testing.T) {
|
|||||||
|
|
||||||
w := createRequest(t, s.CreateHandler, api.CreateRequest{
|
w := createRequest(t, s.CreateHandler, api.CreateRequest{
|
||||||
Model: "bert",
|
Model: "bert",
|
||||||
Files: map[string]string{"file.gguf": digest},
|
Files: []api.File{{Name: "file.gguf", Digest: digest}},
|
||||||
Stream: &stream,
|
Stream: &stream,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1035,7 +1035,7 @@ func TestChatWithPromptEndingInThinkTag(t *testing.T) {
|
|||||||
// Create model with thinking template that adds <think> at the end
|
// Create model with thinking template that adds <think> at the end
|
||||||
w := createRequest(t, s.CreateHandler, api.CreateRequest{
|
w := createRequest(t, s.CreateHandler, api.CreateRequest{
|
||||||
Model: "test-thinking",
|
Model: "test-thinking",
|
||||||
Files: map[string]string{"file.gguf": digest},
|
Files: []api.File{{Name: "file.gguf", Digest: digest}},
|
||||||
Template: `{{- range .Messages }}
|
Template: `{{- range .Messages }}
|
||||||
{{- if eq .Role "user" }}user: {{ .Content }}
|
{{- if eq .Role "user" }}user: {{ .Content }}
|
||||||
{{ else if eq .Role "assistant" }}assistant: {{ if .Thinking }}<think>{{ .Thinking }}</think>{{ end }}{{ .Content }}
|
{{ else if eq .Role "assistant" }}assistant: {{ if .Thinking }}<think>{{ .Thinking }}</think>{{ end }}{{ .Content }}
|
||||||
|
|||||||
@@ -294,7 +294,7 @@ func TestChatHarmonyParserStreamingRealtime(t *testing.T) {
|
|||||||
streamFalse := false
|
streamFalse := false
|
||||||
w := createRequest(t, s.CreateHandler, api.CreateRequest{
|
w := createRequest(t, s.CreateHandler, api.CreateRequest{
|
||||||
Model: "harmony-test-streaming",
|
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 }}`,
|
Template: `<|start|><|end|>{{ with .Tools }}{{ end }}{{ .Prompt }}`,
|
||||||
Stream: &streamFalse,
|
Stream: &streamFalse,
|
||||||
})
|
})
|
||||||
@@ -444,7 +444,7 @@ func TestChatHarmonyParserStreamingSimple(t *testing.T) {
|
|||||||
streamFalse := false
|
streamFalse := false
|
||||||
w := createRequest(t, s.CreateHandler, api.CreateRequest{
|
w := createRequest(t, s.CreateHandler, api.CreateRequest{
|
||||||
Model: "gpt-oss",
|
Model: "gpt-oss",
|
||||||
Files: map[string]string{"test.gguf": digest},
|
Files: []api.File{{Name: "test.gguf", Digest: digest}},
|
||||||
Template: `<|start|><|end|>{{ .Tools }}{{ .Prompt }}`,
|
Template: `<|start|><|end|>{{ .Tools }}{{ .Prompt }}`,
|
||||||
Stream: &streamFalse,
|
Stream: &streamFalse,
|
||||||
})
|
})
|
||||||
@@ -628,7 +628,7 @@ func TestChatHarmonyParserStreaming(t *testing.T) {
|
|||||||
stream := false
|
stream := false
|
||||||
w := createRequest(t, s.CreateHandler, api.CreateRequest{
|
w := createRequest(t, s.CreateHandler, api.CreateRequest{
|
||||||
Model: "harmony-test",
|
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 }}`,
|
Template: `<|start|><|end|>{{ with .Tools }}{{ end }}{{ .Prompt }}`,
|
||||||
Stream: &stream,
|
Stream: &stream,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ func TestList(t *testing.T) {
|
|||||||
|
|
||||||
createRequest(t, s.CreateHandler, api.CreateRequest{
|
createRequest(t, s.CreateHandler, api.CreateRequest{
|
||||||
Name: n,
|
Name: n,
|
||||||
Files: map[string]string{"test.gguf": digest},
|
Files: []api.File{{Name: "test.gguf", Digest: digest}},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ func TestRoutes(t *testing.T) {
|
|||||||
|
|
||||||
r := api.CreateRequest{
|
r := api.CreateRequest{
|
||||||
Name: name,
|
Name: name,
|
||||||
Files: map[string]string{"test.gguf": digest},
|
Files: []api.File{{Name: "test.gguf", Digest: digest}},
|
||||||
Parameters: map[string]any{
|
Parameters: map[string]any{
|
||||||
"seed": 42,
|
"seed": 42,
|
||||||
"top_p": 0.9,
|
"top_p": 0.9,
|
||||||
@@ -343,7 +343,7 @@ func TestRoutes(t *testing.T) {
|
|||||||
stream := false
|
stream := false
|
||||||
createReq := api.CreateRequest{
|
createReq := api.CreateRequest{
|
||||||
Name: "t-bone",
|
Name: "t-bone",
|
||||||
Files: map[string]string{"test.gguf": digest},
|
Files: []api.File{{Name: "test.gguf", Digest: digest}},
|
||||||
Stream: &stream,
|
Stream: &stream,
|
||||||
}
|
}
|
||||||
jsonData, err := json.Marshal(createReq)
|
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
|
// Start with the stable name, and later use a case-shuffled
|
||||||
// version.
|
// version.
|
||||||
Name: wantStableName,
|
Name: wantStableName,
|
||||||
Files: map[string]string{"test.gguf": digest},
|
Files: []api.File{{Name: "test.gguf", Digest: digest}},
|
||||||
Stream: &stream,
|
Stream: &stream,
|
||||||
}))
|
}))
|
||||||
checkManifestList()
|
checkManifestList()
|
||||||
@@ -653,7 +653,7 @@ func TestManifestCaseSensitivity(t *testing.T) {
|
|||||||
t.Logf("creating (again)")
|
t.Logf("creating (again)")
|
||||||
checkOK(createRequest(t, s.CreateHandler, api.CreateRequest{
|
checkOK(createRequest(t, s.CreateHandler, api.CreateRequest{
|
||||||
Name: name(),
|
Name: name(),
|
||||||
Files: map[string]string{"test.gguf": digest},
|
Files: []api.File{{Name: "test.gguf", Digest: digest}},
|
||||||
Stream: &stream,
|
Stream: &stream,
|
||||||
}))
|
}))
|
||||||
checkManifestList()
|
checkManifestList()
|
||||||
@@ -696,7 +696,7 @@ func TestShow(t *testing.T) {
|
|||||||
|
|
||||||
createRequest(t, s.CreateHandler, api.CreateRequest{
|
createRequest(t, s.CreateHandler, api.CreateRequest{
|
||||||
Name: "show-model",
|
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{
|
w := createRequest(t, s.ShowHandler, api.ShowRequest{
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
package syncmap
|
|
||||||
|
|
||||||
import (
|
|
||||||
"maps"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
// SyncMap is a simple, generic thread-safe map implementation.
|
|
||||||
type SyncMap[K comparable, V any] struct {
|
|
||||||
mu sync.RWMutex
|
|
||||||
m map[K]V
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewSyncMap[K comparable, V any]() *SyncMap[K, V] {
|
|
||||||
return &SyncMap[K, V]{
|
|
||||||
m: make(map[K]V),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SyncMap[K, V]) Load(key K) (V, bool) {
|
|
||||||
s.mu.RLock()
|
|
||||||
defer s.mu.RUnlock()
|
|
||||||
val, ok := s.m[key]
|
|
||||||
return val, ok
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SyncMap[K, V]) Store(key K, value V) {
|
|
||||||
s.mu.Lock()
|
|
||||||
defer s.mu.Unlock()
|
|
||||||
s.m[key] = value
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SyncMap[K, V]) Items() map[K]V {
|
|
||||||
s.mu.RLock()
|
|
||||||
defer s.mu.RUnlock()
|
|
||||||
// shallow copy map items
|
|
||||||
return maps.Clone(s.m)
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user