173 lines
5.1 KiB
Go
173 lines
5.1 KiB
Go
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 <model>"))
|
|
})
|
|
|
|
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())
|
|
}
|