Repository: ollama/ollama (language: go)

Issue:
Model saving broken when parent model name has /
### What is the issue?

Using the /save command works for models whose parent doesn't have a slash in the name, but seems broken for models whose parent has a slash.

Example:

~~~sh
[sultan@wailer ~]$ ollama run phi4
>>> /set parameter num_ctx 16384
Set parameter 'num_ctx' to '16384'
>>> /set parameter temperature 0.5
Set parameter 'temperature' to '0.5'
>>> /save phi4-sqk
Created new model 'phi4-sqk'
>>> /bye

[sultan@wailer ~]$ ollama run huihui_ai/phi4-abliterated
>>> /set parameter num_ctx 16384
Set parameter 'num_ctx' to '16384'
>>> /set parameter temperature 0.5
Set parameter 'temperature' to '0.5'
>>> /save phi4-sqk
error: The model name 'phi4-sqk' is invalid
~~~

### Relevant log output

```shell

```

### OS

Linux

### GPU

_No response_

### CPU

Intel

### Ollama version

0.5.13


Discussion / hints:
I've root caused this and what's happening is the `huihui_ai/phi4-abliterated` model was created on Windows and the Ollama client is picking up the file name of the windows blob used to create the model (which is `C:\Users\admin\.ollama\models\blobs\sha256-a47ab2fd4766db9e8ad65b720812d67cbde9404848a4ed9f8c15c50d7e5bd127`) instead of picking the correct model name.

I'm close to a fix but just want to do more testing first.

Fix patch:
diff --git a/cmd/interactive.go b/cmd/interactive.go
index f3489b6525f..d85510d4585 100644
--- a/cmd/interactive.go
+++ b/cmd/interactive.go
@@ -18,6 +18,7 @@ import (
 	"github.com/ollama/ollama/envconfig"
 	"github.com/ollama/ollama/readline"
 	"github.com/ollama/ollama/types/errtypes"
+	"github.com/ollama/ollama/types/model"
 )
 
 type MultilineState int
@@ -459,9 +460,16 @@ func generateInteractive(cmd *cobra.Command, opts runOptions) error {
 }
 
 func NewCreateRequest(name string, opts runOptions) *api.CreateRequest {
+	parentModel := opts.ParentModel
+
+	modelName := model.ParseName(parentModel)
+	if !modelName.IsValid() {
+		parentModel = ""
+	}
+
 	req := &api.CreateRequest{
-		Name: name,
-		From: cmp.Or(opts.ParentModel, opts.Model),
+		Model: name,
+		From:  cmp.Or(parentModel, opts.Model),
 	}
 
 	if opts.System != "" {


Test patch:
diff --git a/cmd/cmd_test.go b/cmd/cmd_test.go
index f21a8f50b3d..41b03e1bd7b 100644
--- a/cmd/cmd_test.go
+++ b/cmd/cmd_test.go
@@ -757,3 +757,132 @@ func TestCreateHandler(t *testing.T) {
 		})
 	}
 }
+
+func TestNewCreateRequest(t *testing.T) {
+	tests := []struct {
+		name     string
+		from     string
+		opts     runOptions
+		expected *api.CreateRequest
+	}{
+		{
+			"basic test",
+			"newmodel",
+			runOptions{
+				Model:       "mymodel",
+				ParentModel: "",
+				Prompt:      "You are a fun AI agent",
+				Messages:    []api.Message{},
+				WordWrap:    true,
+			},
+			&api.CreateRequest{
+				From:  "mymodel",
+				Model: "newmodel",
+			},
+		},
+		{
+			"parent model test",
+			"newmodel",
+			runOptions{
+				Model:       "mymodel",
+				ParentModel: "parentmodel",
+				Messages:    []api.Message{},
+				WordWrap:    true,
+			},
+			&api.CreateRequest{
+				From:  "parentmodel",
+				Model: "newmodel",
+			},
+		},
+		{
+			"parent model as filepath test",
+			"newmodel",
+			runOptions{
+				Model:       "mymodel",
+				ParentModel: "/some/file/like/etc/passwd",
+				Messages:    []api.Message{},
+				WordWrap:    true,
+			},
+			&api.CreateRequest{
+				From:  "mymodel",
+				Model: "newmodel",
+			},
+		},
+		{
+			"parent model as windows filepath test",
+			"newmodel",
+			runOptions{
+				Model:       "mymodel",
+				ParentModel: "D:\\some\\file\\like\\etc\\passwd",
+				Messages:    []api.Message{},
+				WordWrap:    true,
+			},
+			&api.CreateRequest{
+				From:  "mymodel",
+				Model: "newmodel",
+			},
+		},
+		{
+			"options test",
+			"newmodel",
+			runOptions{
+				Model:       "mymodel",
+				ParentModel: "parentmodel",
+				Options: map[string]any{
+					"temperature": 1.0,
+				},
+			},
+			&api.CreateRequest{
+				From:  "parentmodel",
+				Model: "newmodel",
+				Parameters: map[string]any{
+					"temperature": 1.0,
+				},
+			},
+		},
+		{
+			"messages test",
+			"newmodel",
+			runOptions{
+				Model:       "mymodel",
+				ParentModel: "parentmodel",
+				System:      "You are a fun AI agent",
+				Messages: []api.Message{
+					{
+						Role:    "user",
+						Content: "hello there!",
+					},
+					{
+						Role:    "assistant",
+						Content: "hello to you!",
+					},
+				},
+				WordWrap: true,
+			},
+			&api.CreateRequest{
+				From:   "parentmodel",
+				Model:  "newmodel",
+				System: "You are a fun AI agent",
+				Messages: []api.Message{
+					{
+						Role:    "user",
+						Content: "hello there!",
+					},
+					{
+						Role:    "assistant",
+						Content: "hello to you!",
+					},
+				},
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			actual := NewCreateRequest(tt.from, tt.opts)
+			if !cmp.Equal(actual, tt.expected) {
+				t.Errorf("expected output %#v, got %#v", tt.expected, actual)
+			}
+		})
+	}
+}