package cmd import ( "bytes" "crypto/sha256" "encoding/json" "errors" "fmt" "io" "net/http" "net/http/httptest" "os" "path/filepath" "reflect" "strings" "sync" "testing" "time" "github.com/google/go-cmp/cmp" "github.com/spf13/cobra" "github.com/ollama/ollama/api" "github.com/ollama/ollama/manifest" "github.com/ollama/ollama/parser" "github.com/ollama/ollama/progress" "github.com/ollama/ollama/types/model" ) func TestRunThinkingNamesReachServer(t *testing.T) { for _, value := range []string{"xhigh", "minimal", "future", "true", "false"} { t.Run(value, func(t *testing.T) { var got *api.ThinkValue server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/api/show": json.NewEncoder(w).Encode(api.ShowResponse{Capabilities: []model.Capability{model.CapabilityCompletion, model.CapabilityThinking}}) case "/api/generate": var req api.GenerateRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { t.Error(err) } got = req.Think json.NewEncoder(w).Encode(api.GenerateResponse{Done: true}) default: http.NotFound(w, r) } })) defer server.Close() t.Setenv("OLLAMA_HOST", server.URL) cmd := &cobra.Command{} cmd.SetContext(t.Context()) for _, name := range []string{"format", "think", "keepalive"} { cmd.Flags().String(name, "", "") } for _, name := range []string{"verbose", "insecure", "nowordwrap", "hidethinking"} { cmd.Flags().Bool(name, false, "") } if err := cmd.Flags().Set("think", value); err != nil { t.Fatal(err) } if err := RunHandler(cmd, []string{"thinking-test", "hi"}); err != nil { t.Fatal(err) } var want any = value if value == "true" { want = true } else if value == "false" { want = false } if got == nil || got.Value != want { t.Fatalf("think=%v, want %#v", got, want) } }) } } // captureOutput redirects *target (os.Stdout or os.Stderr) to a pipe while fn // runs and returns everything written to it. The original file is restored // before captureOutput returns. func captureOutput(t *testing.T, target **os.File, fn func()) string { t.Helper() old := *target r, w, err := os.Pipe() if err != nil { t.Fatal(err) } *target = w defer func() { *target = old }() done := make(chan string) go func() { var buf bytes.Buffer _, _ = io.Copy(&buf, r) done <- buf.String() }() fn() w.Close() return <-done } func captureStdout(t *testing.T, fn func()) string { t.Helper() return captureOutput(t, &os.Stdout, fn) } func captureStderr(t *testing.T, fn func()) string { t.Helper() return captureOutput(t, &os.Stderr, fn) } func TestShowInfo(t *testing.T) { t.Run("bare details", func(t *testing.T) { var b bytes.Buffer if err := showInfo(&api.ShowResponse{ Details: api.ModelDetails{ Family: "test", ParameterSize: "7B", QuantizationLevel: "FP16", }, }, false, &b); err != nil { t.Fatal(err) } expect := ` Model architecture test parameters 7B quantization FP16 ` if diff := cmp.Diff(expect, b.String()); diff != "" { t.Errorf("unexpected output (-want +got):\n%s", diff) } }) t.Run("renderer and template source", func(t *testing.T) { var b bytes.Buffer if err := showInfo(&api.ShowResponse{ Details: api.ModelDetails{ Family: "test", ParameterSize: "7B", QuantizationLevel: "FP16", }, Renderer: "gemma4-large", Parser: "gemma4", TemplateSource: "renderer_parser", }, false, &b); err != nil { t.Fatal(err) } for _, want := range []string{"renderer", "gemma4-large", "template source", "renderer_parser"} { if !strings.Contains(b.String(), want) { t.Errorf("expected output to contain %q:\n%s", want, b.String()) } } }) t.Run("renderer fields omitted when empty", func(t *testing.T) { var b bytes.Buffer if err := showInfo(&api.ShowResponse{ Details: api.ModelDetails{ Family: "test", ParameterSize: "7B", QuantizationLevel: "FP16", }, TemplateSource: "none", }, false, &b); err != nil { t.Fatal(err) } for _, unwanted := range []string{"renderer", "template source"} { if strings.Contains(b.String(), unwanted) { t.Errorf("expected output to omit %q:\n%s", unwanted, b.String()) } } }) t.Run("bare model info", func(t *testing.T) { var b bytes.Buffer if err := showInfo(&api.ShowResponse{ ModelInfo: map[string]any{ "general.architecture": "test", "general.parameter_count": float64(7_000_000_000), "test.context_length": float64(0), "test.embedding_length": float64(0), }, Details: api.ModelDetails{ Family: "test", ParameterSize: "7B", QuantizationLevel: "FP16", }, }, false, &b); err != nil { t.Fatal(err) } expect := ` Model architecture test parameters 7B context length 0 embedding length 0 quantization FP16 ` if diff := cmp.Diff(expect, b.String()); diff != "" { t.Errorf("unexpected output (-want +got):\n%s", diff) } }) t.Run("verbose model", func(t *testing.T) { var b bytes.Buffer if err := showInfo(&api.ShowResponse{ Details: api.ModelDetails{ Family: "test", ParameterSize: "8B", QuantizationLevel: "FP16", }, Parameters: ` stop up`, ModelInfo: map[string]any{ "general.architecture": "test", "general.parameter_count": float64(8_000_000_000), "some.true_bool": true, "some.false_bool": false, "test.context_length": float64(1000), "test.embedding_length": float64(11434), }, Tensors: []api.Tensor{ {Name: "blk.0.attn_k.weight", Type: "BF16", Shape: []uint64{42, 3117}}, {Name: "blk.0.attn_q.weight", Type: "FP16", Shape: []uint64{3117, 42}}, }, }, true, &b); err != nil { t.Fatal(err) } expect := ` Model architecture test parameters 8B context length 1000 embedding length 11434 quantization FP16 Parameters stop up Metadata general.architecture test general.parameter_count 8e+09 some.false_bool false some.true_bool true test.context_length 1000 test.embedding_length 11434 Tensors blk.0.attn_k.weight BF16 [42 3117] blk.0.attn_q.weight FP16 [3117 42] ` if diff := cmp.Diff(expect, b.String()); diff != "" { t.Errorf("unexpected output (-want +got):\n%s", diff) } }) t.Run("parameters", func(t *testing.T) { var b bytes.Buffer if err := showInfo(&api.ShowResponse{ Details: api.ModelDetails{ Family: "test", ParameterSize: "7B", QuantizationLevel: "FP16", }, Parameters: ` stop never stop gonna stop give stop you stop up temperature 99`, }, false, &b); err != nil { t.Fatal(err) } expect := ` Model architecture test parameters 7B quantization FP16 Parameters stop never stop gonna stop give stop you stop up temperature 99 ` if diff := cmp.Diff(expect, b.String()); diff != "" { t.Errorf("unexpected output (-want +got):\n%s", diff) } }) t.Run("project info", func(t *testing.T) { var b bytes.Buffer if err := showInfo(&api.ShowResponse{ Details: api.ModelDetails{ Family: "test", ParameterSize: "7B", QuantizationLevel: "FP16", }, ProjectorInfo: map[string]any{ "general.architecture": "clip", "general.parameter_count": float64(133_700_000), "clip.vision.embedding_length": float64(0), "clip.vision.projection_dim": float64(0), }, }, false, &b); err != nil { t.Fatal(err) } expect := ` Model architecture test parameters 7B quantization FP16 Projector architecture clip parameters 133.70M embedding length 0 dimensions 0 ` if diff := cmp.Diff(expect, b.String()); diff != "" { t.Errorf("unexpected output (-want +got):\n%s", diff) } }) t.Run("system", func(t *testing.T) { var b bytes.Buffer if err := showInfo(&api.ShowResponse{ Details: api.ModelDetails{ Family: "test", ParameterSize: "7B", QuantizationLevel: "FP16", }, System: `You are a pirate! Ahoy, matey! Weigh anchor! `, }, false, &b); err != nil { t.Fatal(err) } expect := ` Model architecture test parameters 7B quantization FP16 System You are a pirate! Ahoy, matey! ... ` if diff := cmp.Diff(expect, b.String()); diff != "" { t.Errorf("unexpected output (-want +got):\n%s", diff) } }) t.Run("license", func(t *testing.T) { var b bytes.Buffer license := "MIT License\nCopyright (c) Ollama\n" if err := showInfo(&api.ShowResponse{ Details: api.ModelDetails{ Family: "test", ParameterSize: "7B", QuantizationLevel: "FP16", }, License: license, }, false, &b); err != nil { t.Fatal(err) } expect := ` Model architecture test parameters 7B quantization FP16 License MIT License Copyright (c) Ollama ` if diff := cmp.Diff(expect, b.String()); diff != "" { t.Errorf("unexpected output (-want +got):\n%s", diff) } }) t.Run("capabilities", func(t *testing.T) { var b bytes.Buffer if err := showInfo(&api.ShowResponse{ Details: api.ModelDetails{ Family: "test", ParameterSize: "7B", QuantizationLevel: "FP16", }, Capabilities: []model.Capability{model.CapabilityVision, model.CapabilityTools}, }, false, &b); err != nil { t.Fatal(err) } expect := " Model\n" + " architecture test \n" + " parameters 7B \n" + " quantization FP16 \n" + "\n" + " Capabilities\n" + " vision \n" + " tools \n" + "\n" if diff := cmp.Diff(expect, b.String()); diff != "" { t.Errorf("unexpected output (-want +got):\n%s", diff) } }) t.Run("min version", func(t *testing.T) { var b bytes.Buffer if err := showInfo(&api.ShowResponse{ Details: api.ModelDetails{ Family: "test", ParameterSize: "7B", QuantizationLevel: "FP16", }, Requires: "0.19.0", }, false, &b); err != nil { t.Fatal(err) } expect := ` Model architecture test parameters 7B quantization FP16 requires 0.19.0 ` trimLinePadding := func(s string) string { lines := strings.Split(s, "\n") for i, line := range lines { lines[i] = strings.TrimRight(line, " \t\r") } return strings.Join(lines, "\n") } if diff := cmp.Diff(trimLinePadding(expect), trimLinePadding(b.String())); diff != "" { t.Errorf("unexpected output (-want +got):\n%s", diff) } }) } func TestShowInfoRendersRunnerAlternates(t *testing.T) { var b bytes.Buffer if err := showInfo(&api.ShowResponse{ ModelInfo: map[string]any{ "general.architecture": "gemma3", }, Details: api.ModelDetails{ ParameterSize: "4.3B", QuantizationLevel: "nvfp4", }, Manifests: []api.ManifestSummary{ {Digest: "sha256:aaaaaaaaaaaabbbbbbbbbbbbccccccccccccddddddddddddeeeeeeeeeeeeffffffff", Runner: "mlx", Format: "safetensors", Selected: true}, {Digest: "sha256:1111111111112222222222223333333333334444444444445555555555556666", Runner: "llamacpp", Format: "gguf"}, }, }, false, &b); err != nil { t.Fatal(err) } expect := ` Model architecture gemma3 parameters 4.3B quantization nvfp4 runner mlx available runners mlx:sha256:aaaaaaaaaaaa, llamacpp:sha256:111111111111 ` trimLinePadding := func(s string) string { lines := strings.Split(s, "\n") for i, line := range lines { lines[i] = strings.TrimRight(line, " \t\r") } return strings.Join(lines, "\n") } if diff := cmp.Diff(trimLinePadding(expect), trimLinePadding(b.String())); diff != "" { t.Errorf("unexpected output (-want +got):\n%s", diff) } } func TestShowRunnerSummaries(t *testing.T) { selected, ok := showSelectedRunner([]api.ManifestSummary{ {Digest: "sha256:aaaaaaaaaaaabbbbbbbbbbbbccccccccccccddddddddddddeeeeeeeeeeeeffffffff", Runner: "mlx", Format: "safetensors", Selected: true}, {Digest: "sha256:1111111111112222222222223333333333334444444444445555555555556666", Runner: "llamacpp", Format: "gguf"}, }) if !ok || selected != "mlx" { t.Fatalf("selected = %q, %t", selected, ok) } available := showAvailableRunners([]api.ManifestSummary{ {Digest: "sha256:aaaaaaaaaaaabbbbbbbbbbbbccccccccccccddddddddddddeeeeeeeeeeeeffffffff", Runner: "mlx", Format: "safetensors", Selected: true}, {Digest: "sha256:1111111111112222222222223333333333334444444444445555555555556666", Runner: "llamacpp", Format: "gguf"}, }) if available != "mlx:sha256:aaaaaaaaaaaa, llamacpp:sha256:111111111111" { t.Fatalf("available = %q", available) } } func TestDeleteHandler(t *testing.T) { stopped := false mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/api/delete" && r.Method == http.MethodDelete { var req api.DeleteRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } if req.Name == "test-model" { w.WriteHeader(http.StatusOK) } else { w.WriteHeader(http.StatusNotFound) errPayload := `{"error":"model '%s' not found"}` w.Write([]byte(fmt.Sprintf(errPayload, req.Name))) } return } if r.URL.Path == "/api/generate" && r.Method == http.MethodPost { var req api.GenerateRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } if req.Model == "test-model" { w.WriteHeader(http.StatusOK) if err := json.NewEncoder(w).Encode(api.GenerateResponse{ Done: true, }); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } stopped = true return } else { w.WriteHeader(http.StatusNotFound) if err := json.NewEncoder(w).Encode(api.GenerateResponse{ Done: false, }); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } } })) t.Setenv("OLLAMA_HOST", mockServer.URL) t.Cleanup(mockServer.Close) cmd := &cobra.Command{} cmd.SetContext(t.Context()) if err := DeleteHandler(cmd, []string{"test-model"}); err != nil { t.Fatalf("DeleteHandler failed: %v", err) } if !stopped { t.Fatal("Model was not stopped before deletion") } err := DeleteHandler(cmd, []string{"test-model-not-found"}) if err == nil || !strings.Contains(err.Error(), "model 'test-model-not-found' not found") { t.Fatalf("DeleteHandler failed: expected error about stopping non-existent model, got %v", err) } } // Output written to a pipe or file is plain text: word wrapping's cursor // escapes would reach whatever reads it, such as a speech model. func TestRunPipedOutputIsPlain(t *testing.T) { text := "The sky is blue because of a phenomenon called Rayleigh scattering. When sunlight enters the atmosphere it travels through countless tiny gas molecules." mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/api/show": json.NewEncoder(w).Encode(api.ShowResponse{Capabilities: []model.Capability{model.CapabilityCompletion}}) case "/api/generate": for _, word := range strings.SplitAfter(text, " ") { json.NewEncoder(w).Encode(api.GenerateResponse{Response: word}) } json.NewEncoder(w).Encode(api.GenerateResponse{Done: true}) default: http.NotFound(w, r) } })) t.Setenv("OLLAMA_HOST", mockServer.URL) t.Cleanup(mockServer.Close) cmd := &cobra.Command{} cmd.SetContext(t.Context()) for _, f := range []string{"keepalive", "format", "think", "voice"} { cmd.Flags().String(f, "", "") } for _, f := range []string{"verbose", "insecure", "nowordwrap", "hidethinking"} { cmd.Flags().Bool(f, false, "") } var err error out := captureStdout(t, func() { err = RunHandler(cmd, []string{"chat-model", "Why is the sky blue?"}) }) if err != nil { t.Fatal(err) } if strings.Contains(out, "\x1b") || strings.TrimSpace(out) != text { t.Fatalf("piped output %q, want the plain response", out) } } // A model that both chats and speaks chats in ollama run, and speaks the text // when --voice is given. func TestRunChatSpeechModelVoice(t *testing.T) { for _, tt := range []struct { name string voice string speaks bool }{ {name: "chat by default"}, {name: "speak with voice", voice: "default", speaks: true}, } { t.Run(tt.name, func(t *testing.T) { var mu sync.Mutex var speech []map[string]string chats := 0 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { mu.Lock() defer mu.Unlock() switch r.URL.Path { case "/api/show": json.NewEncoder(w).Encode(api.ShowResponse{ Capabilities: []model.Capability{model.CapabilityCompletion, model.CapabilityAudio, model.CapabilitySpeech}, }) case "/v1/audio/speech": var req map[string]string json.NewDecoder(r.Body).Decode(&req) speech = append(speech, req) w.Write(append([]byte("RIFF\x00\x00\x00\x00WAVEfmt \x10\x00\x00\x00\x01\x00\x01\x00\xc0\x5d\x00\x00\x80\xbb\x00\x00\x02\x00\x10\x00data\x00\x00\x00\x00"), make([]byte, 64)...)) case "/api/generate", "/api/chat": chats++ json.NewEncoder(w).Encode(api.GenerateResponse{Response: "hi", Done: true}) default: http.NotFound(w, r) } })) t.Setenv("OLLAMA_HOST", mockServer.URL) t.Cleanup(mockServer.Close) cmd := &cobra.Command{} cmd.SetContext(t.Context()) for _, f := range []string{"keepalive", "format", "think", "voice"} { cmd.Flags().String(f, "", "") } for _, f := range []string{"verbose", "insecure", "nowordwrap", "hidethinking"} { cmd.Flags().Bool(f, false, "") } if tt.voice != "" { if err := cmd.Flags().Set("voice", tt.voice); err != nil { t.Fatal(err) } } var err error out := captureStdout(t, func() { err = RunHandler(cmd, []string{"chat-speech-model", "Hello from Ollama."}) }) if err != nil { t.Fatal(err) } mu.Lock() defer mu.Unlock() if !tt.speaks { if len(speech) != 0 || chats != 1 { t.Fatalf("%d speech and %d chat requests, want a chat", len(speech), chats) } return } if chats != 0 || len(speech) != 1 { t.Fatalf("%d speech and %d chat requests, want speech", len(speech), chats) } if got := speech[0]; got["voice"] != tt.voice || !strings.Contains(got["input"], "Hello from Ollama.") { t.Fatalf("speech request %v", got) } if !strings.HasPrefix(out, "RIFF") { t.Fatalf("output %q, want a WAV", out[:min(len(out), 16)]) } }) } } func TestRunEmbeddingModel(t *testing.T) { reqCh := make(chan api.EmbedRequest, 1) mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/api/show" && r.Method == http.MethodPost { w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(api.ShowResponse{ Capabilities: []model.Capability{model.CapabilityEmbedding}, }); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } return } if r.URL.Path == "/api/embed" && r.Method == http.MethodPost { var req api.EmbedRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } reqCh <- req w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(api.EmbedResponse{ Model: "test-embedding-model", Embeddings: [][]float32{{0.1, 0.2, 0.3}}, }); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } return } http.NotFound(w, r) })) t.Setenv("OLLAMA_HOST", mockServer.URL) t.Cleanup(mockServer.Close) cmd := &cobra.Command{} cmd.SetContext(t.Context()) cmd.Flags().String("keepalive", "", "") cmd.Flags().Bool("truncate", false, "") cmd.Flags().Int("dimensions", 0, "") cmd.Flags().Bool("verbose", false, "") cmd.Flags().Bool("insecure", false, "") cmd.Flags().Bool("nowordwrap", false, "") cmd.Flags().String("format", "", "") cmd.Flags().String("think", "", "") cmd.Flags().Bool("hidethinking", false, "") var err error out := captureStdout(t, func() { err = RunHandler(cmd, []string{"test-embedding-model", "hello", "world"}) }) if err != nil { t.Fatalf("RunHandler returned error: %v", err) } select { case req := <-reqCh: inputText, _ := req.Input.(string) if diff := cmp.Diff("hello world", inputText); diff != "" { t.Errorf("unexpected input (-want +got):\n%s", diff) } if req.Truncate != nil { t.Errorf("expected truncate to be nil, got %v", *req.Truncate) } if req.KeepAlive != nil { t.Errorf("expected keepalive to be nil, got %v", req.KeepAlive) } if req.Dimensions != 0 { t.Errorf("expected dimensions to be 0, got %d", req.Dimensions) } default: t.Fatal("server did not receive embed request") } expectOutput := "[0.1,0.2,0.3]\n" if diff := cmp.Diff(expectOutput, out); diff != "" { t.Errorf("unexpected output (-want +got):\n%s", diff) } } func TestListRunningHandlerShowsRunner(t *testing.T) { mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/api/ps" || r.Method != http.MethodGet { http.NotFound(w, r) return } if err := json.NewEncoder(w).Encode(api.ProcessResponse{ Models: []api.ProcessModelResponse{ { Name: "test-model:latest", Model: "test-model:latest", Digest: "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", Size: 1024, SizeVRAM: 1024, ContextLength: 4096, Runner: "mlx", ExpiresAt: time.Now().Add(time.Hour), }, }, }); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } })) t.Setenv("OLLAMA_HOST", mockServer.URL) t.Cleanup(mockServer.Close) cmd := &cobra.Command{} cmd.SetContext(t.Context()) var err error got := captureStdout(t, func() { err = ListRunningHandler(cmd, nil) }) if err != nil { t.Fatal(err) } for _, want := range []string{"CONTEXT", "RUNNER", "abcdef123456", "mlx"} { if !strings.Contains(got, want) { t.Fatalf("output missing %q:\n%s", want, got) } } } func TestRunHandlerRunnerFlag(t *testing.T) { showReqCh := make(chan api.ShowRequest, 1) generateReqCh := make(chan api.GenerateRequest, 1) mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch { case r.URL.Path == "/api/show" && r.Method == http.MethodPost: var req api.ShowRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } showReqCh <- req w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(api.ShowResponse{ Capabilities: []model.Capability{model.CapabilityCompletion}, ModelInfo: map[string]any{}, }); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } case r.URL.Path == "/api/generate" && r.Method == http.MethodPost: var req api.GenerateRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } generateReqCh <- req w.Header().Set("Content-Type", "application/x-ndjson") if err := json.NewEncoder(w).Encode(api.GenerateResponse{ Model: "test-model", Done: true, }); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } default: http.NotFound(w, r) } })) t.Setenv("OLLAMA_HOST", mockServer.URL) t.Cleanup(mockServer.Close) cmd := &cobra.Command{} cmd.SetContext(t.Context()) cmd.Flags().String("keepalive", "", "") cmd.Flags().Bool("verbose", false, "") cmd.Flags().Bool("insecure", false, "") cmd.Flags().Bool("nowordwrap", false, "") cmd.Flags().String("format", "", "") cmd.Flags().String("runner", "", "") cmd.Flags().String("think", "", "") cmd.Flags().Bool("hidethinking", false, "") if err := cmd.Flags().Set("runner", "llamacpp"); err != nil { t.Fatal(err) } var err error captureStdout(t, func() { err = RunHandler(cmd, []string{"test-model", "hello"}) }) if err != nil { t.Fatal(err) } select { case req := <-showReqCh: if req.Runner != "llamacpp" { t.Fatalf("show runner = %q, want %q", req.Runner, "llamacpp") } default: t.Fatal("server did not receive show request") } select { case req := <-generateReqCh: if req.Runner != "llamacpp" { t.Fatalf("generate runner = %q, want %q", req.Runner, "llamacpp") } default: t.Fatal("server did not receive generate request") } } func TestRunEmbeddingModelWithFlags(t *testing.T) { reqCh := make(chan api.EmbedRequest, 1) mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/api/show" && r.Method == http.MethodPost { w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(api.ShowResponse{ Capabilities: []model.Capability{model.CapabilityEmbedding}, }); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } return } if r.URL.Path == "/api/embed" && r.Method == http.MethodPost { var req api.EmbedRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } reqCh <- req w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(api.EmbedResponse{ Model: "test-embedding-model", Embeddings: [][]float32{{0.4, 0.5}}, LoadDuration: 5 * time.Millisecond, }); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } return } http.NotFound(w, r) })) t.Setenv("OLLAMA_HOST", mockServer.URL) t.Cleanup(mockServer.Close) cmd := &cobra.Command{} cmd.SetContext(t.Context()) cmd.Flags().String("keepalive", "", "") cmd.Flags().Bool("truncate", false, "") cmd.Flags().Int("dimensions", 0, "") cmd.Flags().Bool("verbose", false, "") cmd.Flags().Bool("insecure", false, "") cmd.Flags().Bool("nowordwrap", false, "") cmd.Flags().String("format", "", "") cmd.Flags().String("think", "", "") cmd.Flags().Bool("hidethinking", false, "") if err := cmd.Flags().Set("truncate", "true"); err != nil { t.Fatalf("failed to set truncate flag: %v", err) } if err := cmd.Flags().Set("dimensions", "2"); err != nil { t.Fatalf("failed to set dimensions flag: %v", err) } if err := cmd.Flags().Set("keepalive", "5m"); err != nil { t.Fatalf("failed to set keepalive flag: %v", err) } var err error out := captureStdout(t, func() { err = RunHandler(cmd, []string{"test-embedding-model", "test", "input"}) }) if err != nil { t.Fatalf("RunHandler returned error: %v", err) } select { case req := <-reqCh: inputText, _ := req.Input.(string) if diff := cmp.Diff("test input", inputText); diff != "" { t.Errorf("unexpected input (-want +got):\n%s", diff) } if req.Truncate == nil || !*req.Truncate { t.Errorf("expected truncate pointer true, got %v", req.Truncate) } if req.Dimensions != 2 { t.Errorf("expected dimensions 2, got %d", req.Dimensions) } if req.KeepAlive == nil || req.KeepAlive.Duration != 5*time.Minute { t.Errorf("unexpected keepalive duration: %v", req.KeepAlive) } default: t.Fatal("server did not receive embed request") } expectOutput := "[0.4,0.5]\n" if diff := cmp.Diff(expectOutput, out); diff != "" { t.Errorf("unexpected output (-want +got):\n%s", diff) } } func TestRunEmbeddingModelPipedInput(t *testing.T) { reqCh := make(chan api.EmbedRequest, 1) mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/api/show" && r.Method == http.MethodPost { w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(api.ShowResponse{ Capabilities: []model.Capability{model.CapabilityEmbedding}, }); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } return } if r.URL.Path == "/api/embed" && r.Method == http.MethodPost { var req api.EmbedRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } reqCh <- req w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(api.EmbedResponse{ Model: "test-embedding-model", Embeddings: [][]float32{{0.6, 0.7}}, }); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } return } http.NotFound(w, r) })) t.Setenv("OLLAMA_HOST", mockServer.URL) t.Cleanup(mockServer.Close) cmd := &cobra.Command{} cmd.SetContext(t.Context()) cmd.Flags().String("keepalive", "", "") cmd.Flags().Bool("truncate", false, "") cmd.Flags().Int("dimensions", 0, "") cmd.Flags().Bool("verbose", false, "") cmd.Flags().Bool("insecure", false, "") cmd.Flags().Bool("nowordwrap", false, "") cmd.Flags().String("format", "", "") cmd.Flags().String("think", "", "") cmd.Flags().Bool("hidethinking", false, "") // Capture stdin oldStdin := os.Stdin stdinR, stdinW, _ := os.Pipe() os.Stdin = stdinR stdinW.Write([]byte("piped text")) stdinW.Close() var err error out := captureStdout(t, func() { err = RunHandler(cmd, []string{"test-embedding-model", "additional", "args"}) }) os.Stdin = oldStdin if err != nil { t.Fatalf("RunHandler returned error: %v", err) } select { case req := <-reqCh: inputText, _ := req.Input.(string) // Should combine piped input with command line args if diff := cmp.Diff("piped text additional args", inputText); diff != "" { t.Errorf("unexpected input (-want +got):\n%s", diff) } default: t.Fatal("server did not receive embed request") } expectOutput := "[0.6,0.7]\n" if diff := cmp.Diff(expectOutput, out); diff != "" { t.Errorf("unexpected output (-want +got):\n%s", diff) } } func TestRunEmbeddingModelNoInput(t *testing.T) { mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/api/show" && r.Method == http.MethodPost { w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(api.ShowResponse{ Capabilities: []model.Capability{model.CapabilityEmbedding}, }); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } return } http.NotFound(w, r) })) t.Setenv("OLLAMA_HOST", mockServer.URL) t.Cleanup(mockServer.Close) cmd := &cobra.Command{} cmd.SetContext(t.Context()) cmd.Flags().String("keepalive", "", "") cmd.Flags().Bool("truncate", false, "") cmd.Flags().Int("dimensions", 0, "") cmd.Flags().Bool("verbose", false, "") cmd.Flags().Bool("insecure", false, "") cmd.Flags().Bool("nowordwrap", false, "") cmd.Flags().String("format", "", "") cmd.Flags().String("think", "", "") cmd.Flags().Bool("hidethinking", false, "") cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) // Test with no input arguments (only model name) err := RunHandler(cmd, []string{"test-embedding-model"}) if err == nil || !strings.Contains(err.Error(), "embedding models require input text") { t.Fatalf("expected error about missing input, got %v", err) } } func TestRunHandler_CloudAuthErrorOnShow_PrintsSigninMessage(t *testing.T) { var generateCalled bool mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch { case r.URL.Path == "/api/show" && r.Method == http.MethodPost: w.WriteHeader(http.StatusUnauthorized) if err := json.NewEncoder(w).Encode(map[string]string{ "error": "unauthorized", "signin_url": "https://ollama.com/signin", }); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } return case r.URL.Path == "/api/generate" && r.Method == http.MethodPost: generateCalled = true w.WriteHeader(http.StatusOK) if err := json.NewEncoder(w).Encode(api.GenerateResponse{Done: true}); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } return default: http.NotFound(w, r) } })) t.Setenv("OLLAMA_HOST", mockServer.URL) t.Cleanup(mockServer.Close) cmd := &cobra.Command{} cmd.SetContext(t.Context()) cmd.Flags().String("keepalive", "", "") cmd.Flags().Bool("truncate", false, "") cmd.Flags().Int("dimensions", 0, "") cmd.Flags().Bool("verbose", false, "") cmd.Flags().Bool("insecure", false, "") cmd.Flags().Bool("nowordwrap", false, "") cmd.Flags().String("format", "", "") cmd.Flags().String("think", "", "") cmd.Flags().Bool("hidethinking", false, "") var err error out := captureStdout(t, func() { err = RunHandler(cmd, []string{"gpt-oss:20b:cloud", "hi"}) }) if err != nil { t.Fatalf("RunHandler returned error: %v", err) } if generateCalled { t.Fatal("expected run to stop before /api/generate after unauthorized /api/show") } if !strings.Contains(out, "You need to be signed in to Ollama to run Cloud models.") { t.Fatalf("expected sign-in guidance message, got %q", out) } if !strings.Contains(out, "https://ollama.com/signin") { t.Fatalf("expected signin_url in output, got %q", out) } } func TestRunHandler_CloudAuthErrorOnGenerate_PrintsSigninMessage(t *testing.T) { mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch { case r.URL.Path == "/api/show" && r.Method == http.MethodPost: w.WriteHeader(http.StatusOK) if err := json.NewEncoder(w).Encode(api.ShowResponse{ Capabilities: []model.Capability{model.CapabilityCompletion}, }); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } return case r.URL.Path == "/api/generate" && r.Method == http.MethodPost: w.WriteHeader(http.StatusUnauthorized) if err := json.NewEncoder(w).Encode(map[string]string{ "error": "unauthorized", "signin_url": "https://ollama.com/signin", }); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } return default: http.NotFound(w, r) } })) t.Setenv("OLLAMA_HOST", mockServer.URL) t.Cleanup(mockServer.Close) cmd := &cobra.Command{} cmd.SetContext(t.Context()) cmd.Flags().String("keepalive", "", "") cmd.Flags().Bool("truncate", false, "") cmd.Flags().Int("dimensions", 0, "") cmd.Flags().Bool("verbose", false, "") cmd.Flags().Bool("insecure", false, "") cmd.Flags().Bool("nowordwrap", false, "") cmd.Flags().String("format", "", "") cmd.Flags().String("think", "", "") cmd.Flags().Bool("hidethinking", false, "") var err error out := captureStdout(t, func() { err = RunHandler(cmd, []string{"gpt-oss:20b:cloud", "hi"}) }) if err != nil { t.Fatalf("RunHandler returned error: %v", err) } if !strings.Contains(out, "You need to be signed in to Ollama to run Cloud models.") { t.Fatalf("expected sign-in guidance message, got %q", out) } if !strings.Contains(out, "https://ollama.com/signin") { t.Fatalf("expected signin_url in output, got %q", out) } } func TestRunHandler_ExplicitCloudStubMissing_PullsNormalizedNameTEMP(t *testing.T) { var pulledModel string var generateCalled bool mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch { case r.URL.Path == "/api/show" && r.Method == http.MethodPost: w.WriteHeader(http.StatusOK) if err := json.NewEncoder(w).Encode(api.ShowResponse{ Capabilities: []model.Capability{model.CapabilityCompletion}, RemoteModel: "gpt-oss:20b", }); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } return case r.URL.Path == "/api/tags" && r.Method == http.MethodGet: w.WriteHeader(http.StatusOK) if err := json.NewEncoder(w).Encode(api.ListResponse{Models: nil}); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } return case r.URL.Path == "/api/pull" && r.Method == http.MethodPost: var req api.PullRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } pulledModel = req.Model w.WriteHeader(http.StatusOK) if err := json.NewEncoder(w).Encode(api.ProgressResponse{Status: "success"}); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } return case r.URL.Path == "/api/generate" && r.Method == http.MethodPost: generateCalled = true w.WriteHeader(http.StatusOK) if err := json.NewEncoder(w).Encode(api.GenerateResponse{Done: true}); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } return default: http.NotFound(w, r) } })) t.Setenv("OLLAMA_HOST", mockServer.URL) t.Cleanup(mockServer.Close) cmd := &cobra.Command{} cmd.SetContext(t.Context()) cmd.Flags().String("keepalive", "", "") cmd.Flags().Bool("truncate", false, "") cmd.Flags().Int("dimensions", 0, "") cmd.Flags().Bool("verbose", false, "") cmd.Flags().Bool("insecure", false, "") cmd.Flags().Bool("nowordwrap", false, "") cmd.Flags().String("format", "", "") cmd.Flags().String("think", "", "") cmd.Flags().Bool("hidethinking", false, "") err := RunHandler(cmd, []string{"gpt-oss:20b:cloud", "hi"}) if err != nil { t.Fatalf("RunHandler returned error: %v", err) } if pulledModel != "gpt-oss:20b-cloud" { t.Fatalf("expected normalized pull model %q, got %q", "gpt-oss:20b-cloud", pulledModel) } if !generateCalled { t.Fatal("expected /api/generate to be called") } } func TestRunHandler_ExplicitCloudStubPresent_SkipsPullTEMP(t *testing.T) { var pullCalled bool var generateCalled bool mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch { case r.URL.Path == "/api/show" && r.Method == http.MethodPost: w.WriteHeader(http.StatusOK) if err := json.NewEncoder(w).Encode(api.ShowResponse{ Capabilities: []model.Capability{model.CapabilityCompletion}, RemoteModel: "gpt-oss:20b", }); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } return case r.URL.Path == "/api/tags" && r.Method == http.MethodGet: w.WriteHeader(http.StatusOK) if err := json.NewEncoder(w).Encode(api.ListResponse{ Models: []api.ListModelResponse{{Name: "gpt-oss:20b-cloud"}}, }); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } return case r.URL.Path == "/api/pull" && r.Method == http.MethodPost: pullCalled = true w.WriteHeader(http.StatusOK) if err := json.NewEncoder(w).Encode(api.ProgressResponse{Status: "success"}); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } return case r.URL.Path == "/api/generate" && r.Method == http.MethodPost: generateCalled = true w.WriteHeader(http.StatusOK) if err := json.NewEncoder(w).Encode(api.GenerateResponse{Done: true}); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } return default: http.NotFound(w, r) } })) t.Setenv("OLLAMA_HOST", mockServer.URL) t.Cleanup(mockServer.Close) cmd := &cobra.Command{} cmd.SetContext(t.Context()) cmd.Flags().String("keepalive", "", "") cmd.Flags().Bool("truncate", false, "") cmd.Flags().Int("dimensions", 0, "") cmd.Flags().Bool("verbose", false, "") cmd.Flags().Bool("insecure", false, "") cmd.Flags().Bool("nowordwrap", false, "") cmd.Flags().String("format", "", "") cmd.Flags().String("think", "", "") cmd.Flags().Bool("hidethinking", false, "") err := RunHandler(cmd, []string{"gpt-oss:20b:cloud", "hi"}) if err != nil { t.Fatalf("RunHandler returned error: %v", err) } if pullCalled { t.Fatal("expected /api/pull not to be called when cloud stub already exists") } if !generateCalled { t.Fatal("expected /api/generate to be called") } } func TestRunHandler_ExplicitCloudStubPullFailure_IsBestEffortTEMP(t *testing.T) { var generateCalled bool mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch { case r.URL.Path == "/api/show" && r.Method == http.MethodPost: w.WriteHeader(http.StatusOK) if err := json.NewEncoder(w).Encode(api.ShowResponse{ Capabilities: []model.Capability{model.CapabilityCompletion}, RemoteModel: "gpt-oss:20b", }); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } return case r.URL.Path == "/api/tags" && r.Method == http.MethodGet: w.WriteHeader(http.StatusOK) if err := json.NewEncoder(w).Encode(api.ListResponse{Models: nil}); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } return case r.URL.Path == "/api/pull" && r.Method == http.MethodPost: w.WriteHeader(http.StatusInternalServerError) if err := json.NewEncoder(w).Encode(map[string]string{"error": "pull failed"}); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } return case r.URL.Path == "/api/generate" && r.Method == http.MethodPost: generateCalled = true w.WriteHeader(http.StatusOK) if err := json.NewEncoder(w).Encode(api.GenerateResponse{Done: true}); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } return default: http.NotFound(w, r) } })) t.Setenv("OLLAMA_HOST", mockServer.URL) t.Cleanup(mockServer.Close) cmd := &cobra.Command{} cmd.SetContext(t.Context()) cmd.Flags().String("keepalive", "", "") cmd.Flags().Bool("truncate", false, "") cmd.Flags().Int("dimensions", 0, "") cmd.Flags().Bool("verbose", false, "") cmd.Flags().Bool("insecure", false, "") cmd.Flags().Bool("nowordwrap", false, "") cmd.Flags().String("format", "", "") cmd.Flags().String("think", "", "") cmd.Flags().Bool("hidethinking", false, "") err := RunHandler(cmd, []string{"gpt-oss:20b:cloud", "hi"}) if err != nil { t.Fatalf("RunHandler returned error: %v", err) } if !generateCalled { t.Fatal("expected /api/generate to be called despite pull failure") } } func TestGetModelfileName(t *testing.T) { tests := []struct { name string modelfileName string fileExists bool expectedName string expectedErr error }{ { name: "no modelfile specified, no modelfile exists", modelfileName: "", fileExists: false, expectedName: "", expectedErr: os.ErrNotExist, }, { name: "no modelfile specified, modelfile exists", modelfileName: "", fileExists: true, expectedName: "Modelfile", expectedErr: nil, }, { name: "modelfile specified, no modelfile exists", modelfileName: "crazyfile", fileExists: false, expectedName: "", expectedErr: os.ErrNotExist, }, { name: "modelfile specified, modelfile exists", modelfileName: "anotherfile", fileExists: true, expectedName: "anotherfile", expectedErr: nil, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cmd := &cobra.Command{ Use: "fakecmd", } cmd.Flags().String("file", "", "path to modelfile") var expectedFilename string if tt.fileExists { var fn string if tt.modelfileName != "" { fn = tt.modelfileName } else { fn = "Modelfile" } tempFile, err := os.CreateTemp(t.TempDir(), fn) if err != nil { t.Fatalf("temp modelfile creation failed: %v", err) } defer tempFile.Close() expectedFilename = tempFile.Name() err = cmd.Flags().Set("file", expectedFilename) if err != nil { t.Fatalf("couldn't set file flag: %v", err) } } else { expectedFilename = tt.expectedName if tt.modelfileName != "" { err := cmd.Flags().Set("file", tt.modelfileName) if err != nil { t.Fatalf("couldn't set file flag: %v", err) } } } actualFilename, actualErr := getModelfileName(cmd) if actualFilename != expectedFilename { t.Errorf("expected filename: '%s' actual filename: '%s'", expectedFilename, actualFilename) } if tt.expectedErr != os.ErrNotExist { if actualErr != tt.expectedErr { t.Errorf("expected err: %v actual err: %v", tt.expectedErr, actualErr) } } else { if !os.IsNotExist(actualErr) { t.Errorf("expected err: %v actual err: %v", tt.expectedErr, actualErr) } } }) } } func TestPushHandler(t *testing.T) { tests := []struct { name string modelName string serverResponse map[string]func(w http.ResponseWriter, r *http.Request) expectedError string expectedOutput string }{ { name: "successful push", modelName: "test-model", serverResponse: map[string]func(w http.ResponseWriter, r *http.Request){ "/api/push": func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { t.Errorf("expected POST request, got %s", r.Method) } var req api.PushRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } if req.Name != "test-model" { t.Errorf("expected model name 'test-model', got %s", req.Name) } // Simulate progress updates responses := []api.ProgressResponse{ {Status: "preparing manifest"}, {Digest: "sha256:abc123456789", Total: 100, Completed: 50}, {Digest: "sha256:abc123456789", Total: 100, Completed: 100}, } for _, resp := range responses { if err := json.NewEncoder(w).Encode(resp); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.(http.Flusher).Flush() } }, "/api/me": func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { t.Errorf("expected POST request, got %s", r.Method) } }, }, expectedOutput: "\nYou can find your model at:\n\n\thttps://ollama.com/test-model\n", }, { name: "not signed in push", modelName: "notsignedin-model", serverResponse: map[string]func(w http.ResponseWriter, r *http.Request){ "/api/me": func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { t.Errorf("expected POST request, got %s", r.Method) } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusUnauthorized) err := json.NewEncoder(w).Encode(map[string]string{ "error": "unauthorized", "signin_url": "https://somethingsomething", }) if err != nil { t.Fatal(err) } }, }, expectedOutput: "You need to be signed in to push", }, { name: "unauthorized push", modelName: "unauthorized-model", serverResponse: map[string]func(w http.ResponseWriter, r *http.Request){ "/api/push": func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusUnauthorized) err := json.NewEncoder(w).Encode(map[string]string{ "error": "403: {\"errors\":[{\"code\":\"ACCESS DENIED\", \"message\":\"access denied\"}]}", }) if err != nil { t.Fatal(err) } }, "/api/me": func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { t.Errorf("expected POST request, got %s", r.Method) } }, }, expectedError: "you are not authorized to push to this namespace, create the model under a namespace you own", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if handler, ok := tt.serverResponse[r.URL.Path]; ok { handler(w, r) return } http.Error(w, "not found", http.StatusNotFound) })) defer mockServer.Close() t.Setenv("OLLAMA_HOST", mockServer.URL) tmpDir := t.TempDir() t.Setenv("HOME", tmpDir) t.Setenv("USERPROFILE", tmpDir) initializeKeypair() cmd := &cobra.Command{} cmd.Flags().Bool("insecure", false, "") cmd.SetContext(t.Context()) var err error var stdout string // Capture stderr for progress output and stdout for the "Model // pushed" message. captureStderr(t, func() { stdout = captureStdout(t, func() { err = PushHandler(cmd, []string{tt.modelName}) }) }) if tt.expectedError == "" { if err != nil { t.Errorf("expected no error, got %v", err) } if tt.expectedOutput != "" { if !strings.Contains(stdout, tt.expectedOutput) { t.Errorf("expected output %q, got %q", tt.expectedOutput, stdout) } } } else { if err == nil || !strings.Contains(err.Error(), tt.expectedError) { t.Errorf("expected error containing %q, got %v", tt.expectedError, err) } } }) } } func TestListHandler(t *testing.T) { tests := []struct { name string args []string serverResponse []api.ListModelResponse expectedError string expectedOutput string }{ { name: "list all models", args: []string{}, serverResponse: []api.ListModelResponse{ {Name: "model1", Digest: "abc123abc123abc123", Size: 1024, ModifiedAt: time.Now().Add(-24 * time.Hour)}, {Name: "model2", Digest: "def456def456def456", Size: 2048, ModifiedAt: time.Now().Add(-48 * time.Hour)}, }, expectedOutput: "NAME ID SIZE MODIFIED \n" + "model1 abc123abc123 1.0 KB 24 hours ago \n" + "model2 def456def456 2.0 KB 2 days ago \n", }, { name: "filter models by prefix", args: []string{"model1"}, serverResponse: []api.ListModelResponse{ {Name: "model1", Digest: "abc123abc123abc123", Size: 1024, ModifiedAt: time.Now().Add(-24 * time.Hour)}, {Name: "model2", Digest: "def456def456def456", Size: 2048, ModifiedAt: time.Now().Add(-24 * time.Hour)}, }, expectedOutput: "NAME ID SIZE MODIFIED \n" + "model1 abc123abc123 1.0 KB 24 hours ago \n", }, { name: "server error", args: []string{}, expectedError: "server error", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/api/tags" || r.Method != http.MethodGet { t.Errorf("unexpected request to %s %s", r.Method, r.URL.Path) http.Error(w, "not found", http.StatusNotFound) return } if tt.expectedError != "" { http.Error(w, tt.expectedError, http.StatusInternalServerError) return } response := api.ListResponse{Models: tt.serverResponse} if err := json.NewEncoder(w).Encode(response); err != nil { t.Fatal(err) } })) defer mockServer.Close() t.Setenv("OLLAMA_HOST", mockServer.URL) cmd := &cobra.Command{} cmd.SetContext(t.Context()) var err error output := captureStdout(t, func() { err = ListHandler(cmd, tt.args) }) if tt.expectedError == "" { if err != nil { t.Errorf("expected no error, got %v", err) } if output != tt.expectedOutput { t.Errorf("expected output:\n%s\ngot:\n%s", tt.expectedOutput, output) } } else { if err == nil || !strings.Contains(err.Error(), tt.expectedError) { t.Errorf("expected error containing %q, got %v", tt.expectedError, err) } } }) } } func TestCreateHandler(t *testing.T) { tests := []struct { name string modelName string modelFile string serverResponse map[string]func(w http.ResponseWriter, r *http.Request) expectedError string expectedOutput string }{ { name: "successful create", modelName: "test-model", modelFile: "FROM foo", serverResponse: map[string]func(w http.ResponseWriter, r *http.Request){ "/api/create": func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { t.Errorf("expected POST request, got %s", r.Method) } req := api.CreateRequest{} if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } if req.Model != "test-model" { t.Errorf("expected model name 'test-model', got %s", req.Name) } if req.From != "foo" { t.Errorf("expected from 'foo', got %s", req.From) } responses := []api.ProgressResponse{ {Status: "using existing layer sha256:56bb8bd477a519ffa694fc449c2413c6f0e1d3b1c88fa7e3c9d88d3ae49d4dcb"}, {Status: "writing manifest"}, {Status: "success"}, } for _, resp := range responses { if err := json.NewEncoder(w).Encode(resp); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.(http.Flusher).Flush() } }, }, expectedOutput: "", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodHead && r.URL.Path == "/" { return } handler, ok := tt.serverResponse[r.URL.Path] if !ok { t.Errorf("unexpected request to %s", r.URL.Path) http.Error(w, "not found", http.StatusNotFound) return } handler(w, r) })) t.Setenv("OLLAMA_HOST", mockServer.URL) t.Cleanup(mockServer.Close) tempFile, err := os.CreateTemp(t.TempDir(), "modelfile") if err != nil { t.Fatal(err) } defer os.Remove(tempFile.Name()) if _, err := tempFile.WriteString(tt.modelFile); err != nil { t.Fatal(err) } if err := tempFile.Close(); err != nil { t.Fatal(err) } cmd := &cobra.Command{} cmd.Flags().String("file", "", "") if err := cmd.Flags().Set("file", tempFile.Name()); err != nil { t.Fatal(err) } cmd.Flags().Bool("insecure", false, "") cmd.SetContext(t.Context()) var stdout string // Capture stderr for progress output and stdout for the final // status message. captureStderr(t, func() { stdout = captureStdout(t, func() { err = CreateHandler(cmd, []string{tt.modelName}) }) }) if tt.expectedError == "" { if err != nil { t.Errorf("expected no error, got %v", err) } if tt.expectedOutput != "" { if stdout != tt.expectedOutput { t.Errorf("expected output %q, got %q", tt.expectedOutput, stdout) } } } }) } } func TestCreateRequestFileNamesPreservesModelDirectoryLayout(t *testing.T) { root := t.TempDir() files := map[string]string{ filepath.Join(root, "model.safetensors"): "sha256:model", filepath.Join(root, "config.json"): "sha256:config", filepath.Join(root, "2_Dense", "config.json"): "sha256:dense-config", filepath.Join(root, "2_Dense", "model.safetensors"): "sha256:dense-model", } got := createRequestFileNames(files) want := map[string]string{ filepath.Join(root, "model.safetensors"): "model.safetensors", filepath.Join(root, "config.json"): "config.json", filepath.Join(root, "2_Dense", "config.json"): "2_Dense/config.json", filepath.Join(root, "2_Dense", "model.safetensors"): "2_Dense/model.safetensors", } if diff := cmp.Diff(want, got); diff != "" { t.Fatalf("mismatch (-want +got):\n%s", diff) } } func TestCreateRequestFileNamesPreservesRelativeModelDirectoryLayout(t *testing.T) { root := t.TempDir() t.Chdir(root) files := map[string]string{ "model.safetensors": "sha256:model", "config.json": "sha256:config", "2_Dense/config.json": "sha256:dense-config", "2_Dense/model.safetensors": "sha256:dense-model", "3_Dense/config.json": "sha256:dense-config", "3_Dense/model.safetensors": "sha256:dense-model", } got := createRequestFileNames(files) for file := range files { if got[file] != filepath.ToSlash(file) { t.Fatalf("%s = %q, want %q", file, got[file], filepath.ToSlash(file)) } } } func TestCreateHandlerDraftQuantizeRequiresDraft(t *testing.T) { dir := t.TempDir() modelfile := filepath.Join(dir, "Modelfile") if err := os.WriteFile(modelfile, []byte("FROM base\n"), 0o644); err != nil { t.Fatal(err) } cmd := &cobra.Command{} cmd.Flags().Bool("experimental", false, "") cmd.Flags().String("file", modelfile, "") cmd.Flags().String("draft-quantize", "mxfp8", "") cmd.SetContext(t.Context()) err := CreateHandler(cmd, []string{"test-model"}) if err == nil || !strings.Contains(err.Error(), "--draft-quantize requires a DRAFT model") { t.Fatalf("error = %v, want draft-quantize requires DRAFT", err) } } func TestCreateHandlerRejectsGGUFQuantizeBeforeUpload(t *testing.T) { dir := t.TempDir() modelfile := filepath.Join(dir, "Modelfile") if err := os.WriteFile(modelfile, []byte("FROM base\n"), 0o644); err != nil { t.Fatal(err) } cmd := &cobra.Command{} cmd.Flags().String("file", modelfile, "") cmd.Flags().String("quantize", "Q4_K_M", "") cmd.Flags().String("draft-quantize", "", "") cmd.Flags().Bool("force", false, "") cmd.SetContext(t.Context()) err := CreateHandler(cmd, []string{"test-model"}) if err == nil || !strings.Contains(err.Error(), "quantize GGUF models") { t.Fatalf("error = %v, want GGUF quantization error", err) } } func TestCreateHandlerRejectsForceForGGUF(t *testing.T) { dir := t.TempDir() modelfile := filepath.Join(dir, "Modelfile") if err := os.WriteFile(modelfile, []byte("FROM base\n"), 0o644); err != nil { t.Fatal(err) } cmd := &cobra.Command{} cmd.Flags().String("file", modelfile, "") cmd.Flags().String("quantize", "", "") cmd.Flags().String("draft-quantize", "", "") cmd.Flags().Bool("force", true, "") cmd.SetContext(t.Context()) err := CreateHandler(cmd, []string{"test-model"}) if err == nil || !strings.Contains(err.Error(), "--force is only supported for local MLX safetensors imports") { t.Fatalf("error = %v, want GGUF force error", err) } } func TestSharedBlobStore(t *testing.T) { t.Setenv("OLLAMA_MODELS", t.TempDir()) blobs, err := manifest.BlobsPath("") if err != nil { t.Fatal(err) } assertNoBlobs := func(t *testing.T) { t.Helper() entries, err := os.ReadDir(blobs) if err != nil { t.Fatal(err) } if len(entries) != 0 { t.Fatalf("blob store has %d entries after the probe, want 0", len(entries)) } } // A server that stats the same blob directory this process writes to. shared := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodHead { w.WriteHeader(http.StatusNotFound) return } blob, err := manifest.BlobsPath(strings.TrimPrefix(r.URL.Path, "/api/blobs/")) if err != nil { w.WriteHeader(http.StatusBadRequest) return } if _, err := os.Stat(blob); err != nil { w.WriteHeader(http.StatusNotFound) return } w.WriteHeader(http.StatusOK) })) defer shared.Close() separate := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotFound) })) defer separate.Close() t.Run("shared store", func(t *testing.T) { t.Setenv("OLLAMA_HOST", shared.URL) client, err := api.ClientFromEnvironment() if err != nil { t.Fatal(err) } if !sharedBlobStore(t.Context(), client) { t.Fatal("sharedBlobStore() = false, want true") } assertNoBlobs(t) }) t.Run("separate store", func(t *testing.T) { t.Setenv("OLLAMA_HOST", separate.URL) client, err := api.ClientFromEnvironment() if err != nil { t.Fatal(err) } if sharedBlobStore(t.Context(), client) { t.Fatal("sharedBlobStore() = true, want false") } assertNoBlobs(t) }) t.Run("OLLAMA_CREATE_REMOTE forces upload", func(t *testing.T) { t.Setenv("OLLAMA_HOST", shared.URL) t.Setenv("OLLAMA_CREATE_REMOTE", "1") client, err := api.ClientFromEnvironment() if err != nil { t.Fatal(err) } if sharedBlobStore(t.Context(), client) { t.Fatal("sharedBlobStore() = true, want false") } assertNoBlobs(t) }) } // blobServer mocks the blob endpoints: HEAD reports what it has, POST stores. type blobServer struct { mu sync.Mutex have map[string]bool posts int } func (s *blobServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { digest := strings.TrimPrefix(r.URL.Path, "/api/blobs/") s.mu.Lock() defer s.mu.Unlock() switch r.Method { case http.MethodHead: if s.have[digest] { w.WriteHeader(http.StatusOK) } else { w.WriteHeader(http.StatusNotFound) } case http.MethodPost: io.Copy(io.Discard, r.Body) s.have[digest] = true s.posts++ w.WriteHeader(http.StatusCreated) default: w.WriteHeader(http.StatusNotFound) } } func TestCreateBlob(t *testing.T) { t.Setenv("OLLAMA_MODELS", t.TempDir()) src := filepath.Join(t.TempDir(), "model.gguf") data := []byte("blob contents") if err := os.WriteFile(src, data, 0o644); err != nil { t.Fatal(err) } digest := fmt.Sprintf("sha256:%x", sha256.Sum256(data)) newClient := func(t *testing.T, have ...string) (*api.Client, *blobServer) { t.Helper() bs := &blobServer{have: make(map[string]bool)} for _, d := range have { bs.have[d] = true } server := httptest.NewServer(bs) t.Cleanup(server.Close) t.Setenv("OLLAMA_HOST", server.URL) client, err := api.ClientFromEnvironment() if err != nil { t.Fatal(err) } return client, bs } cmd := &cobra.Command{} cmd.SetContext(t.Context()) p := progress.NewProgress(io.Discard) defer p.Stop() t.Run("skips blobs the server already has", func(t *testing.T) { client, bs := newClient(t, digest) if _, err := createBlob(cmd, client, src, digest, p, false); err != nil { t.Fatal(err) } if bs.posts != 0 { t.Fatalf("posts = %d, want 0", bs.posts) } }) t.Run("uploads to a separate store", func(t *testing.T) { client, bs := newClient(t) if _, err := createBlob(cmd, client, src, digest, p, false); err != nil { t.Fatal(err) } if bs.posts != 1 || !bs.have[digest] { t.Fatalf("posts = %d, have = %v, want one upload of %s", bs.posts, bs.have, digest) } }) t.Run("writes directly to a shared store", func(t *testing.T) { client, bs := newClient(t) if _, err := createBlob(cmd, client, src, digest, p, true); err != nil { t.Fatal(err) } if bs.posts != 0 { t.Fatalf("posts = %d, want 0", bs.posts) } blob, err := manifest.BlobsPath(digest) if err != nil { t.Fatal(err) } got, err := os.ReadFile(blob) if err != nil { t.Fatal(err) } if !bytes.Equal(got, data) { t.Fatalf("blob contents = %q, want %q", got, data) } }) t.Run("rejects a file whose digest changed", func(t *testing.T) { client, _ := newClient(t) _, err := createBlob(cmd, client, src, "sha256:"+strings.Repeat("0", 64), p, true) if err == nil || !strings.Contains(err.Error(), "changed during create") { t.Fatalf("error = %v, want digest mismatch", err) } }) } func TestCreateHandlerRejectsAdaptersBeforeUpload(t *testing.T) { dir := t.TempDir() modelfile := filepath.Join(dir, "Modelfile") if err := os.WriteFile(modelfile, []byte("FROM base\nADAPTER ./adapter.gguf\n"), 0o644); err != nil { t.Fatal(err) } cmd := &cobra.Command{} cmd.Flags().String("file", modelfile, "") cmd.Flags().String("quantize", "", "") cmd.Flags().String("draft-quantize", "", "") cmd.Flags().Bool("force", false, "") cmd.SetContext(t.Context()) if err := CreateHandler(cmd, []string{"test-model"}); !errors.Is(err, errAdaptersUnsupported) { t.Fatalf("error = %v, want %v", err, errAdaptersUnsupported) } } func TestCreateHandlerRejectsTypicalPBeforeUpload(t *testing.T) { t.Setenv("OLLAMA_HOST", "127.0.0.1:0") dir := t.TempDir() modelfile := filepath.Join(dir, "Modelfile") if err := os.WriteFile(modelfile, []byte("FROM base\nPARAMETER typical_p 0.5\n"), 0o644); err != nil { t.Fatal(err) } cmd := &cobra.Command{} cmd.Flags().String("file", modelfile, "") cmd.Flags().String("quantize", "", "") cmd.Flags().String("draft-quantize", "", "") cmd.Flags().Bool("force", false, "") cmd.SetContext(t.Context()) if err := CreateHandler(cmd, []string{"test-model"}); !errors.Is(err, errTypicalPDeprecated) { t.Fatalf("error = %v, want %v", err, errTypicalPDeprecated) } } func TestCreateHandlerRejectsForceForRemoteSafetensors(t *testing.T) { t.Setenv("OLLAMA_CREATE_REMOTE", "1") dir := t.TempDir() modelDir := filepath.Join(dir, "model") if err := os.Mkdir(modelDir, 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(modelDir, "config.json"), []byte(`{"architectures":["Qwen3ForCausalLM"]}`), 0o644); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(modelDir, "model.safetensors"), []byte("dummy"), 0o644); err != nil { t.Fatal(err) } modelfile := filepath.Join(dir, "Modelfile") if err := os.WriteFile(modelfile, []byte("FROM "+modelDir+"\n"), 0o644); err != nil { t.Fatal(err) } cmd := &cobra.Command{} cmd.Flags().String("file", modelfile, "") cmd.Flags().String("quantize", "", "") cmd.Flags().String("draft-quantize", "", "") cmd.Flags().Bool("force", true, "") cmd.SetContext(t.Context()) err := CreateHandler(cmd, []string{"test-model"}) if err == nil || !strings.Contains(err.Error(), "--force is only supported for local MLX safetensors imports") { t.Fatalf("error = %v, want remote force error", err) } } func TestResolveCreateLocalModelDir(t *testing.T) { dir := t.TempDir() modelfile := filepath.Join(dir, "Modelfile") modelDir := filepath.Join(dir, "model") if err := os.Mkdir(modelDir, 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(modelDir, "config.json"), []byte(`{}`), 0o644); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(modelDir, "model.safetensors"), []byte("dummy"), 0o644); err != nil { t.Fatal(err) } if got := resolveCreateLocalModelDir("gemma4", modelfile); got != "gemma4" { t.Fatalf("resolveCreateLocalModelDir(model name) = %q, want gemma4", got) } if got := resolveCreateLocalModelDir("./model", modelfile); got != modelDir { t.Fatalf("resolveCreateLocalModelDir(local dir) = %q, want %q", got, modelDir) } } func TestResolveCreateDraftDir(t *testing.T) { dir := t.TempDir() modelfile := filepath.Join(dir, "Modelfile") draftDir := filepath.Join(dir, "assistant") if err := os.Mkdir(draftDir, 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(draftDir, "config.json"), []byte(`{}`), 0o644); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(draftDir, "model.safetensors"), []byte("dummy"), 0o644); err != nil { t.Fatal(err) } got, err := resolveCreateDraftDir("./assistant", modelfile) if err != nil { t.Fatal(err) } if got != draftDir { t.Fatalf("resolveCreateDraftDir(local dir) = %q, want %q", got, draftDir) } _, err = resolveCreateDraftDir("assistant-model", modelfile) if err == nil || !strings.Contains(err.Error(), "DRAFT model references must be local safetensors directories") { t.Fatalf("error = %v, want unsupported draft model reference", err) } } func TestCreateHandlerManifestList(t *testing.T) { mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/api/create" { t.Errorf("unexpected request to %s", r.URL.Path) http.Error(w, "not found", http.StatusNotFound) return } if r.Method != http.MethodPost { t.Errorf("expected POST request, got %s", r.Method) } var req api.CreateRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } if req.Model != "parent" { t.Errorf("model = %q, want %q", req.Model, "parent") } if !cmp.Equal(req.List, []string{"gguf", "safetensors"}) { t.Errorf("list = %#v, want %#v", req.List, []string{"gguf", "safetensors"}) } if req.From != "" || len(req.Files) > 0 { t.Errorf("manifest list create sent normal create fields: from=%q files=%v", req.From, req.Files) } if err := json.NewEncoder(w).Encode(api.ProgressResponse{Status: "success"}); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.(http.Flusher).Flush() })) t.Setenv("OLLAMA_HOST", mockServer.URL) t.Cleanup(mockServer.Close) cmd := &cobra.Command{} cmd.Flags().String("file", "", "") cmd.Flags().String("quantize", "", "") cmd.Flags().Bool("experimental", false, "") cmd.Flags().StringSlice("combine", nil, "") cmd.SetContext(t.Context()) if err := cmd.Flags().Set("combine", "gguf,safetensors"); err != nil { t.Fatal(err) } var err error captureStderr(t, func() { err = CreateHandler(cmd, []string{"parent"}) }) if err != nil { t.Fatal(err) } } func TestSafetensorsCreateOptionsDetectsLocalDir(t *testing.T) { dir := t.TempDir() modelDir := filepath.Join(dir, "model") draftDir := filepath.Join(dir, "assistant") for _, d := range []string{modelDir, draftDir} { if err := os.Mkdir(d, 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(d, "config.json"), []byte(`{"architectures":["Qwen3ForCausalLM"]}`), 0o644); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(d, "model.safetensors"), []byte("dummy"), 0o644); err != nil { t.Fatal(err) } } modelfilePath := filepath.Join(dir, "Modelfile") modelfile, err := parser.ParseFile(strings.NewReader("FROM ./model\nDRAFT ./assistant\n")) if err != nil { t.Fatal(err) } opts, ok, err := safetensorsCreateOptions(modelfile, modelfilePath, "test-model") if err != nil { t.Fatal(err) } if !ok { t.Fatal("safetensorsCreateOptions did not detect safetensors model") } if opts.ModelName != "test-model" || opts.ModelDir != modelDir { t.Fatalf("opts model/name = %q/%q, want test-model/%s", opts.ModelName, opts.ModelDir, modelDir) } if opts.Modelfile == nil || opts.Modelfile.Draft != draftDir { t.Fatalf("draft dir = %v, want %s", opts.Modelfile, draftDir) } for _, tt := range []struct { name string modelfile string want string }{ { name: "multiple model sources", modelfile: "FROM ./model\nFROM ./model\n", want: "exactly one FROM source", }, { name: "multiple draft sources", modelfile: "FROM ./model\nDRAFT ./assistant\nDRAFT ./assistant\n", want: "at most one DRAFT source", }, } { t.Run(tt.name, func(t *testing.T) { modelfile, err := parser.ParseFile(strings.NewReader(tt.modelfile)) if err != nil { t.Fatal(err) } _, _, err = safetensorsCreateOptions(modelfile, modelfilePath, "test-model") if err == nil || !strings.Contains(err.Error(), tt.want) { t.Fatalf("error = %v, want %q", err, tt.want) } }) } } func TestSafetensorsCreateOptionsLeavesGGUFDraftOnStandardPath(t *testing.T) { modelfile, err := parser.ParseFile(strings.NewReader("FROM ./model.gguf\nDRAFT ./draft.gguf\n")) if err != nil { t.Fatal(err) } _, gotCreate, err := safetensorsCreateOptions(modelfile, filepath.Join(t.TempDir(), "Modelfile"), "test-model") if err != nil { t.Fatal(err) } if gotCreate { t.Fatal("GGUF draft was routed through safetensors create") } } func TestSafetensorsCreateOptionsPreservesRequires(t *testing.T) { dir := t.TempDir() modelDir := filepath.Join(dir, "model") if err := os.Mkdir(modelDir, 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(modelDir, "config.json"), []byte(`{"architectures":["Qwen3ForCausalLM"]}`), 0o644); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(modelDir, "model.safetensors"), []byte("dummy"), 0o644); err != nil { t.Fatal(err) } modelfilePath := filepath.Join(dir, "Modelfile") for _, tt := range []struct { name string modelfile string wantCreate bool wantRequires string }{ { name: "safetensors", modelfile: "FROM ./model\nREQUIRES 0.14.0\n", wantCreate: true, wantRequires: "0.14.0", }, { name: "GGUF", modelfile: "FROM ./model.gguf\nREQUIRES 0.14.0\n", }, } { t.Run(tt.name, func(t *testing.T) { modelfile, err := parser.ParseFile(strings.NewReader(tt.modelfile)) if err != nil { t.Fatal(err) } opts, gotCreate, err := safetensorsCreateOptions(modelfile, modelfilePath, "test-model") if err != nil { t.Fatal(err) } if gotCreate != tt.wantCreate { t.Fatalf("safetensors create = %v, want %v", gotCreate, tt.wantCreate) } if gotCreate && opts.Modelfile.Requires != tt.wantRequires { t.Fatalf("requires = %q, want %q", opts.Modelfile.Requires, tt.wantRequires) } }) } } 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", }, }, { "explicit cloud model preserves source when parent lacks it", "newmodel", runOptions{ Model: "qwen3.5:cloud", ParentModel: "qwen3.5", Messages: []api.Message{}, WordWrap: true, }, &api.CreateRequest{ From: "qwen3.5:cloud", 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!", }, }, }, }, { "loaded messages are preserved when saving", "newmodel", runOptions{ Model: "mymodel", ParentModel: "parentmodel", LoadedMessages: []api.Message{{Role: "assistant", Content: "loaded"}}, Messages: []api.Message{{Role: "user", Content: "new"}}, }, &api.CreateRequest{ From: "parentmodel", Model: "newmodel", Messages: []api.Message{ {Role: "assistant", Content: "loaded"}, {Role: "user", Content: "new"}, }, }, }, } 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) } }) } } func TestApplyShowResponseToRunOptions(t *testing.T) { opts := runOptions{} info := &api.ShowResponse{ Details: api.ModelDetails{ ParentModel: "parentmodel", }, Messages: []api.Message{ {Role: "assistant", Content: "loaded"}, }, } applyShowResponseToRunOptions(&opts, info) if opts.ParentModel != "parentmodel" { t.Fatalf("ParentModel = %q, want %q", opts.ParentModel, "parentmodel") } if !cmp.Equal(opts.LoadedMessages, info.Messages) { t.Fatalf("LoadedMessages = %#v, want %#v", opts.LoadedMessages, info.Messages) } info.Messages[0].Content = "modified" if opts.LoadedMessages[0].Content == "modified" { t.Fatal("LoadedMessages should be copied independently from ShowResponse") } } func TestApplyShowResponseRefreshesMultimodal(t *testing.T) { for _, tt := range []struct { name string info api.ShowResponse }{ { name: "decision vision", info: api.ShowResponse{Capabilities: []model.Capability{model.CapabilityDecision, model.CapabilityVision}}, }, { name: "audio", info: api.ShowResponse{Capabilities: []model.Capability{model.CapabilityAudio}}, }, { name: "legacy projector", info: api.ShowResponse{ProjectorInfo: map[string]any{"general.architecture": "clip"}}, }, { name: "legacy vision", info: api.ShowResponse{ModelInfo: map[string]any{"mllama.vision.block_count": 32}}, }, } { t.Run(tt.name, func(t *testing.T) { opts := runOptions{} applyShowResponseToRunOptions(&opts, &tt.info) if !opts.MultiModal { t.Fatal("loading a multimodal model did not enable media input") } applyShowResponseToRunOptions(&opts, &api.ShowResponse{ Capabilities: []model.Capability{model.CapabilityDecision}, }) if opts.MultiModal { t.Fatal("loading a text-only model did not disable media input") } }) } } func TestRunOptions_Copy(t *testing.T) { // Setup test data originalKeepAlive := &api.Duration{Duration: 5 * time.Minute} originalThink := &api.ThinkValue{Value: "test reasoning"} original := runOptions{ Model: "test-model", ParentModel: "parent-model", LoadedMessages: []api.Message{{Role: "assistant", Content: "loaded hello"}}, Prompt: "test prompt", Messages: []api.Message{ {Role: "user", Content: "hello"}, {Role: "assistant", Content: "hi there"}, }, WordWrap: true, Format: "json", System: "system prompt", Images: []api.ImageData{ []byte("image1"), []byte("image2"), }, Options: map[string]any{ "temperature": 0.7, "max_tokens": 1000, "top_p": 0.9, }, MultiModal: true, KeepAlive: originalKeepAlive, Think: originalThink, HideThinking: false, ShowConnect: true, } // Test the copy copied := original.Copy() // Test 1: Verify the copy is not the same instance if &copied == &original { t.Error("Copy should return a different instance") } // Test 2: Verify all fields are copied correctly tests := []struct { name string got interface{} want interface{} }{ {"Model", copied.Model, original.Model}, {"ParentModel", copied.ParentModel, original.ParentModel}, {"LoadedMessages", copied.LoadedMessages, original.LoadedMessages}, {"Prompt", copied.Prompt, original.Prompt}, {"WordWrap", copied.WordWrap, original.WordWrap}, {"Format", copied.Format, original.Format}, {"System", copied.System, original.System}, {"MultiModal", copied.MultiModal, original.MultiModal}, {"HideThinking", copied.HideThinking, original.HideThinking}, {"ShowConnect", copied.ShowConnect, original.ShowConnect}, } for _, tt := range tests { if !reflect.DeepEqual(tt.got, tt.want) { t.Errorf("%s mismatch: got %v, want %v", tt.name, tt.got, tt.want) } } // Test 3: Verify Messages slice is deeply copied if len(copied.Messages) != len(original.Messages) { t.Errorf("Messages length mismatch: got %d, want %d", len(copied.Messages), len(original.Messages)) } if len(copied.Messages) > 0 && &copied.Messages[0] == &original.Messages[0] { t.Error("Messages should be different instances") } // Modify original to verify independence if len(original.Messages) > 0 { originalContent := original.Messages[0].Content original.Messages[0].Content = "modified" if len(copied.Messages) > 0 && copied.Messages[0].Content == "modified" { t.Error("Messages should be independent after copy") } // Restore for other tests original.Messages[0].Content = originalContent } // Test 4: Verify Images slice is deeply copied if len(copied.Images) != len(original.Images) { t.Errorf("Images length mismatch: got %d, want %d", len(copied.Images), len(original.Images)) } if len(copied.Images) > 0 && &copied.Images[0] == &original.Images[0] { t.Error("Images should be different instances") } // Modify original to verify independence if len(original.Images) > 0 { originalImage := original.Images[0] original.Images[0] = []byte("modified") if len(copied.Images) > 0 && string(copied.Images[0]) == "modified" { t.Error("Images should be independent after copy") } // Restore for other tests original.Images[0] = originalImage } // Test 5: Verify Options map is deeply copied if len(copied.Options) != len(original.Options) { t.Errorf("Options length mismatch: got %d, want %d", len(copied.Options), len(original.Options)) } if len(copied.Options) > 0 && &copied.Options == &original.Options { t.Error("Options map should be different instances") } // Modify original to verify independence if len(original.Options) > 0 { originalTemp := original.Options["temperature"] original.Options["temperature"] = 0.9 if copied.Options["temperature"] == 0.9 { t.Error("Options should be independent after copy") } // Restore for other tests original.Options["temperature"] = originalTemp } // Test 6: Verify KeepAlive pointer is copied (shallow copy) if copied.KeepAlive != original.KeepAlive { t.Error("KeepAlive pointer should be the same (shallow copy)") } // Test 7: Verify Think pointer creates a new instance if original.Think != nil && copied.Think == original.Think { t.Error("Think should be a different instance") } if original.Think != nil && copied.Think != nil { if !reflect.DeepEqual(copied.Think.Value, original.Think.Value) { t.Errorf("Think.Value mismatch: got %v, want %v", copied.Think.Value, original.Think.Value) } } // Test 8: Test with zero values zeroOriginal := runOptions{} zeroCopy := zeroOriginal.Copy() if !reflect.DeepEqual(zeroCopy, zeroOriginal) { fmt.Printf("orig: %#v\ncopy: %#v\n", zeroOriginal, zeroCopy) t.Error("Copy of zero value should equal original zero value") } } func TestRunOptions_Copy_EmptySlicesAndMaps(t *testing.T) { // Test with empty slices and maps original := runOptions{ LoadedMessages: []api.Message{}, Messages: []api.Message{}, Images: []api.ImageData{}, Options: map[string]any{}, } copied := original.Copy() if copied.LoadedMessages == nil { t.Error("Empty LoadedMessages slice should remain empty, not nil") } if copied.Messages == nil { t.Error("Empty Messages slice should remain empty, not nil") } if copied.Images == nil { t.Error("Empty Images slice should remain empty, not nil") } if copied.Options == nil { t.Error("Empty Options map should remain empty, not nil") } if len(copied.Messages) != 0 { t.Error("Empty Messages slice should remain empty") } if len(copied.LoadedMessages) != 0 { t.Error("Empty LoadedMessages slice should remain empty") } if len(copied.Images) != 0 { t.Error("Empty Images slice should remain empty") } if len(copied.Options) != 0 { t.Error("Empty Options map should remain empty") } } func TestRunOptions_Copy_NilPointers(t *testing.T) { // Test with nil pointers original := runOptions{ KeepAlive: nil, Think: nil, } copied := original.Copy() if copied.KeepAlive != nil { t.Error("Nil KeepAlive should remain nil") } if copied.Think != nil { t.Error("Nil Think should remain nil") } } func TestRunOptions_Copy_ThinkValueVariants(t *testing.T) { tests := []struct { name string think *api.ThinkValue }{ {"nil Think", nil}, {"bool true", &api.ThinkValue{Value: true}}, {"bool false", &api.ThinkValue{Value: false}}, {"string value", &api.ThinkValue{Value: "reasoning text"}}, {"int value", &api.ThinkValue{Value: 42}}, {"nil value", &api.ThinkValue{Value: nil}}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { original := runOptions{Think: tt.think} copied := original.Copy() if tt.think == nil { if copied.Think != nil { t.Error("Nil Think should remain nil") } return } if copied.Think == nil { t.Error("Non-nil Think should not become nil") return } if copied.Think == original.Think { t.Error("Think should be a different instance") } if !reflect.DeepEqual(copied.Think.Value, original.Think.Value) { t.Errorf("Think.Value mismatch: got %v, want %v", copied.Think.Value, original.Think.Value) } }) } } func TestShowInfoImageCapability(t *testing.T) { var b bytes.Buffer err := showInfo(&api.ShowResponse{ Details: api.ModelDetails{ Family: "ZImagePipeline", ParameterSize: "10.3B", QuantizationLevel: "Q8", }, Capabilities: []model.Capability{model.CapabilityImage}, Requires: "0.19.0", }, false, &b) if err != nil { t.Fatal(err) } expect := " Model\n" + " architecture ZImagePipeline \n" + " parameters 10.3B \n" + " quantization Q8 \n" + " requires 0.19.0 \n" + "\n" + " Capabilities\n" + " image \n" + "\n" if diff := cmp.Diff(expect, b.String()); diff != "" { t.Errorf("unexpected output (-want +got):\n%s", diff) } } func TestShowInfoSpeechVoices(t *testing.T) { var b bytes.Buffer err := showInfo(&api.ShowResponse{ Details: api.ModelDetails{Family: "phonon", ParameterSize: "31M", QuantizationLevel: "F32"}, Capabilities: []model.Capability{model.CapabilitySpeech}, Voices: []string{"default", "Fairy Godmother", "Zoey"}, }, false, &b) if err != nil { t.Fatal(err) } expect := " Model\n" + " architecture phonon \n" + " parameters 31M \n" + " quantization F32 \n" + "\n" + " Capabilities\n" + " speech \n" + "\n" + " Voices\n" + " default \n" + " Fairy Godmother \n" + " Zoey \n" + "\n" if diff := cmp.Diff(expect, b.String()); diff != "" { t.Errorf("unexpected output (-want +got):\n%s", diff) } } func TestPushProgressMessage(t *testing.T) { tests := []struct { name string status string digest string wantMsg string }{ { name: "uses status when provided", status: "uploading model", digest: "sha256:abc123456789def", wantMsg: "uploading model", }, { name: "falls back to digest when status empty", status: "", digest: "sha256:abc123456789def", wantMsg: "pushing abc123456789...", }, { name: "handles short digest gracefully", status: "", digest: "sha256:abc", wantMsg: "pushing sha256:abc...", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { msg := tt.status if msg == "" { if len(tt.digest) >= 19 { msg = fmt.Sprintf("pushing %s...", tt.digest[7:19]) } else { msg = fmt.Sprintf("pushing %s...", tt.digest) } } if msg != tt.wantMsg { t.Errorf("got %q, want %q", msg, tt.wantMsg) } }) } } func TestRunOptions_Copy_Independence(t *testing.T) { // Test that modifications to original don't affect copy originalThink := &api.ThinkValue{Value: "original"} original := runOptions{ Model: "original-model", LoadedMessages: []api.Message{{Role: "assistant", Content: "loaded"}}, Messages: []api.Message{{Role: "user", Content: "original"}}, Options: map[string]any{"key": "value"}, Think: originalThink, } copied := original.Copy() // Modify original original.Model = "modified-model" if len(original.LoadedMessages) > 0 { original.LoadedMessages[0].Content = "modified loaded" } if len(original.Messages) > 0 { original.Messages[0].Content = "modified" } original.Options["key"] = "modified" if original.Think != nil { original.Think.Value = "modified" } // Verify copy is unchanged if copied.Model == "modified-model" { t.Error("Copy Model should not be affected by original modification") } if len(copied.LoadedMessages) > 0 && copied.LoadedMessages[0].Content == "modified loaded" { t.Error("Copy LoadedMessages should not be affected by original modification") } if len(copied.Messages) > 0 && copied.Messages[0].Content == "modified" { t.Error("Copy Messages should not be affected by original modification") } if copied.Options["key"] == "modified" { t.Error("Copy Options should not be affected by original modification") } if copied.Think != nil && copied.Think.Value == "modified" { t.Error("Copy Think should not be affected by original modification") } } func TestLoadOrUnloadModel_CloudModelAuth(t *testing.T) { tests := []struct { name string model string showStatus int remoteHost string remoteModel string whoamiStatus int whoamiResp any expectWhoami bool expectedError string expectAuthError bool }{ { name: "ollama.com cloud model - user signed in", model: "test-cloud-model", remoteHost: "https://ollama.com", remoteModel: "test-model", whoamiStatus: http.StatusOK, whoamiResp: api.UserResponse{Name: "testuser"}, expectWhoami: true, }, { name: "ollama.com cloud model - user not signed in", model: "test-cloud-model", remoteHost: "https://ollama.com", remoteModel: "test-model", whoamiStatus: http.StatusUnauthorized, whoamiResp: map[string]string{ "error": "unauthorized", "signin_url": "https://ollama.com/signin", }, expectWhoami: true, expectedError: "unauthorized", expectAuthError: true, }, { name: "non-ollama.com remote - no auth check", model: "test-cloud-model", remoteHost: "https://other-remote.com", remoteModel: "test-model", whoamiStatus: http.StatusUnauthorized, // should not be called whoamiResp: nil, }, { name: "explicit :cloud model - auth check without remote metadata", model: "kimi-k2.5:cloud", remoteHost: "", remoteModel: "", whoamiStatus: http.StatusOK, whoamiResp: api.UserResponse{Name: "testuser"}, expectWhoami: true, }, { name: "explicit :cloud model without local stub returns not found by default", model: "minimax-m2.7:cloud", showStatus: http.StatusNotFound, whoamiStatus: http.StatusOK, whoamiResp: api.UserResponse{Name: "testuser"}, expectedError: "not found", expectWhoami: false, expectAuthError: false, }, { name: "explicit -cloud model - auth check without remote metadata", model: "kimi-k2.5:latest-cloud", remoteHost: "", remoteModel: "", whoamiStatus: http.StatusOK, whoamiResp: api.UserResponse{Name: "testuser"}, expectWhoami: true, }, { name: "dash cloud-like name without explicit source does not require auth", model: "test-cloud-model", remoteHost: "", remoteModel: "", whoamiStatus: http.StatusUnauthorized, // should not be called whoamiResp: nil, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { whoamiCalled := false mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/api/show": if tt.showStatus != 0 && tt.showStatus != http.StatusOK { w.WriteHeader(tt.showStatus) _ = json.NewEncoder(w).Encode(map[string]string{"error": "not found"}) return } w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(api.ShowResponse{ RemoteHost: tt.remoteHost, RemoteModel: tt.remoteModel, }); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } case "/api/me": whoamiCalled = true w.Header().Set("Content-Type", "application/json") w.WriteHeader(tt.whoamiStatus) if tt.whoamiResp != nil { if err := json.NewEncoder(w).Encode(tt.whoamiResp); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } case "/api/generate": w.WriteHeader(http.StatusOK) default: http.NotFound(w, r) } })) defer mockServer.Close() t.Setenv("OLLAMA_HOST", mockServer.URL) cmd := &cobra.Command{} cmd.SetContext(t.Context()) opts := &runOptions{ Model: tt.model, ShowConnect: false, } err := loadOrUnloadModel(cmd, opts) if whoamiCalled != tt.expectWhoami { t.Errorf("whoami called = %v, want %v", whoamiCalled, tt.expectWhoami) } if tt.expectedError != "" { if err == nil { t.Errorf("expected error containing %q, got nil", tt.expectedError) } else { if !tt.expectAuthError && !strings.Contains(strings.ToLower(err.Error()), strings.ToLower(tt.expectedError)) { t.Errorf("expected error containing %q, got %v", tt.expectedError, err) } if tt.expectAuthError { var authErr api.AuthorizationError if !errors.As(err, &authErr) { t.Errorf("expected AuthorizationError, got %T: %v", err, err) } } } } else { if err != nil { t.Errorf("expected no error, got %v", err) } } }) } } func TestIsLocalhost(t *testing.T) { tests := []struct { name string host string expected bool }{ {"default empty", "", true}, {"localhost no port", "localhost", true}, {"localhost with port", "localhost:11435", true}, {"127.0.0.1 no port", "127.0.0.1", true}, {"127.0.0.1 with port", "127.0.0.1:11434", true}, {"0.0.0.0 no port", "0.0.0.0", true}, {"0.0.0.0 with port", "0.0.0.0:11434", true}, {"::1 no port", "::1", true}, {"[::1] with port", "[::1]:11434", true}, {"loopback with scheme", "http://localhost:11434", true}, {"remote hostname", "example.com", false}, {"remote hostname with port", "example.com:11434", false}, {"remote IP", "192.168.1.1", false}, {"remote IP with port", "192.168.1.1:11434", false}, {"remote with scheme", "http://example.com:11434", false}, {"https remote", "https://example.com:443", false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Setenv("OLLAMA_HOST", tt.host) got := isLocalhost() if got != tt.expected { t.Errorf("isLocalhost() with OLLAMA_HOST=%q = %v, want %v", tt.host, got, tt.expected) } }) } } func TestRunCommandHasNoAgentFlags(t *testing.T) { root := NewCLI() run, _, err := root.Find([]string{"run"}) if err != nil { t.Fatal(err) } for _, name := range []string{"resume", "headless", "auto-approve-tools", "skill", "experimental", "experimental-yolo", "experimental-websearch"} { if flag := run.Flags().Lookup(name); flag != nil { t.Errorf("run command still exposes former agent flag --%s", name) } } } func TestFormerAgentEntryPointsAreRejected(t *testing.T) { tests := [][]string{ {"run", "llama3", "--resume"}, {"run", "llama3", "--headless"}, {"run", "llama3", "--auto-approve-tools"}, {"run", "llama3", "--skill", "release-notes"}, {"run", "llama3", "--experimental"}, {"run", "llama3", "--experimental-yolo"}, {"run", "llama3", "--experimental-websearch"}, {"agent"}, } for _, args := range tests { t.Run(strings.Join(args, " "), func(t *testing.T) { root := NewCLI() root.SetArgs(args) err := root.Execute() if err == nil { t.Fatalf("former agent entry point %q succeeded", args) } if !strings.Contains(err.Error(), "unknown") { t.Fatalf("former agent entry point %q returned %v, want unknown command or flag", args, err) } }) } }