diff --git a/x/create/client/create.go b/x/create/client/create.go index a6f1cd0f4..1a5968c5f 100644 --- a/x/create/client/create.go +++ b/x/create/client/create.go @@ -16,6 +16,8 @@ import ( "slices" "strings" + "golang.org/x/mod/semver" + "github.com/ollama/ollama/api" "github.com/ollama/ollama/manifest" modelparsers "github.com/ollama/ollama/model/parsers" @@ -38,6 +40,7 @@ type ModelfileConfig struct { Draft string Parser string Renderer string + Requires string Parameters map[string]any } @@ -75,7 +78,20 @@ func ConfigFromModelfile(modelfile *parser.Modelfile) (string, *ModelfileConfig, mfConfig.Parser = cmd.Args case "renderer": mfConfig.Renderer = cmd.Args - case "adapter", "message", "requires": + case "requires": + requires := cmd.Args + if !strings.HasPrefix(requires, "v") { + requires = "v" + requires + } + if !semver.IsValid(requires) { + return "", nil, fmt.Errorf("requires must be a valid semver (e.g. 0.14.0)") + } + minVersion := "v" + MinOllamaVersion + if semver.Compare(requires, minVersion) < 0 { + return "", nil, fmt.Errorf("requires %s is below the minimum supported version %s for safetensors models", strings.TrimPrefix(requires, "v"), MinOllamaVersion) + } + mfConfig.Requires = strings.TrimPrefix(requires, "v") + case "adapter", "message": continue default: if slices.Contains(ignoredModelfileParameters, cmd.Name) { @@ -518,6 +534,9 @@ func newManifestWriter(opts CreateOptions, capabilities []string, parserName, re } configData.Capabilities = caps configData.Requires = MinOllamaVersion + if opts.Modelfile != nil && opts.Modelfile.Requires != "" { + configData.Requires = opts.Modelfile.Requires + } configData.Parser = resolveParserName(opts.Modelfile, parserName) configData.Renderer = resolveRendererName(opts.Modelfile, rendererName) if opts.Modelfile != nil && opts.Modelfile.Draft != "" { diff --git a/x/create/client/create_test.go b/x/create/client/create_test.go index 47cd9d8b3..e1a9fd2f1 100644 --- a/x/create/client/create_test.go +++ b/x/create/client/create_test.go @@ -46,6 +46,7 @@ func TestConfigFromModelfile(t *testing.T) { FROM ./model DRAFT ./assistant TEMPLATE {{ .Prompt }} +REQUIRES 0.20.0 PARAMETER temperature 0.7 PARAMETER stop USER: PARAMETER stop ASSISTANT: @@ -71,6 +72,10 @@ PARAMETER stop ASSISTANT: t.Fatalf("Draft = %q, want %q", mfConfig.Draft, "./assistant") } + if mfConfig.Requires != "0.20.0" { + t.Fatalf("Requires = %q, want %q", mfConfig.Requires, "0.20.0") + } + if got := mfConfig.Parameters["temperature"]; got != float32(0.7) { t.Fatalf("temperature = %#v, want %v", got, float32(0.7)) } @@ -80,6 +85,42 @@ PARAMETER stop ASSISTANT: } } +func TestConfigFromModelfile_RequiresBelowMinimum(t *testing.T) { + modelfile, err := parser.ParseFile(strings.NewReader(` +FROM ./model +REQUIRES 0.14.0 +`)) + if err != nil { + t.Fatal(err) + } + + _, _, err = ConfigFromModelfile(modelfile) + if err == nil { + t.Fatal("expected error for REQUIRES below minimum, got nil") + } + if !strings.Contains(err.Error(), "minimum supported version") { + t.Fatalf("error = %v, want error mentioning minimum supported version", err) + } +} + +func TestConfigFromModelfile_RequiresInvalidSemver(t *testing.T) { + modelfile, err := parser.ParseFile(strings.NewReader(` +FROM ./model +REQUIRES not-a-version +`)) + if err != nil { + t.Fatal(err) + } + + _, _, err = ConfigFromModelfile(modelfile) + if err == nil { + t.Fatal("expected error for invalid semver, got nil") + } + if !strings.Contains(err.Error(), "valid semver") { + t.Fatalf("error = %v, want semver error", err) + } +} + func TestModelfileConfig_Empty(t *testing.T) { config := &ModelfileConfig{}