package chat import ( "bytes" "encoding/json" "fmt" "io" "net/http" "net/http/httptest" "strings" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) var _ = Describe("Run chat", func() { It("streams a single chat response", func() { var capturedModel string var capturedAuth string server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/v1/models" { w.Header().Set("Content-Type", "application/json") writeResponse(w, `{"object":"list","data":[{"id":"test-model","object":"model"}]}`) return } Expect(r.URL.Path).To(Equal("/v1/chat/completions")) capturedAuth = r.Header.Get("Authorization") var body struct { Model string `json:"model"` Messages []struct { Role string `json:"role"` Content string `json:"content"` } `json:"messages"` } Expect(json.NewDecoder(r.Body).Decode(&body)).To(Succeed()) capturedModel = body.Model Expect(body.Messages).To(HaveLen(1)) Expect(body.Messages[0].Role).To(Equal("user")) Expect(body.Messages[0].Content).To(Equal("hello")) w.Header().Set("Content-Type", "text/event-stream") writeResponse(w, "data: {\"choices\":[{\"index\":0,\"delta\":{\"content\":\"hi\"}}]}\n\n") writeResponse(w, "data: {\"choices\":[{\"index\":0,\"delta\":{\"content\":\"!\"}}]}\n\n") writeResponse(w, "data: [DONE]\n\n") })) defer server.Close() var out bytes.Buffer err := Run(GinkgoT().Context(), Options{ Model: "test-model", BaseURL: server.URL + "/v1", APIKey: "secret", In: strings.NewReader("hello\n/exit\n"), Out: &out, }) Expect(err).ToNot(HaveOccurred()) Expect(capturedModel).To(Equal("test-model")) Expect(capturedAuth).To(Equal("Bearer secret")) Expect(out.String()).To(ContainSubstring("assistant: hi!")) Expect(out.String()).To(ContainSubstring("bye")) }) It("auto-selects the only available model", func() { server := chatTestServer([]string{"solo"}, nil) defer server.Close() var out bytes.Buffer err := Run(GinkgoT().Context(), Options{ BaseURL: server.URL + "/v1", In: strings.NewReader("/exit\n"), Out: &out, }) Expect(err).ToNot(HaveOccurred()) Expect(out.String()).To(ContainSubstring("LocalAI chat (solo)")) }) It("returns an actionable error when no models are installed", func() { server := chatTestServer(nil, nil) defer server.Close() err := Run(GinkgoT().Context(), Options{ BaseURL: server.URL + "/v1", In: strings.NewReader(""), }) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("no chat models are installed")) Expect(err.Error()).To(ContainSubstring("local-ai models install ")) }) It("returns an actionable error when multiple models are available without a selection", func() { server := chatTestServer([]string{"alpha", "beta"}, nil) defer server.Close() err := Run(GinkgoT().Context(), Options{ BaseURL: server.URL + "/v1", In: strings.NewReader(""), }) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("multiple models are available")) Expect(err.Error()).To(ContainSubstring("--model")) Expect(err.Error()).To(ContainSubstring("alpha")) Expect(err.Error()).To(ContainSubstring("beta")) }) It("lists and switches models inside the chat", func() { requestedModels := []string{} server := chatTestServer([]string{"alpha", "beta"}, func(model string) { requestedModels = append(requestedModels, model) }) defer server.Close() var out bytes.Buffer err := Run(GinkgoT().Context(), Options{ Model: "alpha", BaseURL: server.URL + "/v1", In: strings.NewReader("/models\n/model beta\nhello\n/exit\n"), Out: &out, }) Expect(err).ToNot(HaveOccurred()) Expect(out.String()).To(ContainSubstring("* alpha")) Expect(out.String()).To(ContainSubstring(" beta")) Expect(out.String()).To(ContainSubstring("switched to beta; conversation cleared")) Expect(requestedModels).To(Equal([]string{"beta"})) }) }) func chatTestServer(models []string, onChat func(model string)) *httptest.Server { return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/v1/models": w.Header().Set("Content-Type", "application/json") writeResponse(w, `{"object":"list","data":[`) for i, model := range models { if i > 0 { writeResponse(w, ",") } writeResponsef(w, `{"id":%q,"object":"model"}`, model) } writeResponse(w, `]}`) case "/v1/chat/completions": var body struct { Model string `json:"model"` } Expect(json.NewDecoder(r.Body).Decode(&body)).To(Succeed()) if onChat != nil { onChat(body.Model) } w.Header().Set("Content-Type", "text/event-stream") writeResponse(w, "data: {\"choices\":[{\"index\":0,\"delta\":{\"content\":\"ok\"}}]}\n\n") writeResponse(w, "data: [DONE]\n\n") default: w.WriteHeader(http.StatusNotFound) } })) } func writeResponse(w io.Writer, text string) { _, err := fmt.Fprint(w, text) Expect(err).ToNot(HaveOccurred()) } func writeResponsef(w io.Writer, format string, args ...any) { _, err := fmt.Fprintf(w, format, args...) Expect(err).ToNot(HaveOccurred()) }