Skip to content

Commit 48a8ce9

Browse files
authored
fix(cli): handle chat output errors (#10229)
Propagate terminal write errors from the chat prompt and explicitly ignore stream close errors during cleanup. Update chat tests to assert response writer errors so errcheck passes without hiding failed writes. Tests: - go test -count=1 ./core/cli/chat - go test -count=1 ./core/cli Assisted-by: Codex:GPT-5 Signed-off-by: Ching Kao <0980124jim@gmail.com>
1 parent 8344d1c commit 48a8ce9

3 files changed

Lines changed: 63 additions & 27 deletions

File tree

core/cli/chat/chat_test.go

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"bytes"
55
"encoding/json"
66
"fmt"
7+
"io"
78
"net/http"
89
"net/http/httptest"
910
"strings"
@@ -19,7 +20,7 @@ var _ = Describe("Run chat", func() {
1920
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
2021
if r.URL.Path == "/v1/models" {
2122
w.Header().Set("Content-Type", "application/json")
22-
fmt.Fprint(w, `{"object":"list","data":[{"id":"test-model","object":"model"}]}`)
23+
writeResponse(w, `{"object":"list","data":[{"id":"test-model","object":"model"}]}`)
2324
return
2425
}
2526

@@ -40,9 +41,9 @@ var _ = Describe("Run chat", func() {
4041
Expect(body.Messages[0].Content).To(Equal("hello"))
4142

4243
w.Header().Set("Content-Type", "text/event-stream")
43-
fmt.Fprint(w, "data: {\"choices\":[{\"index\":0,\"delta\":{\"content\":\"hi\"}}]}\n\n")
44-
fmt.Fprint(w, "data: {\"choices\":[{\"index\":0,\"delta\":{\"content\":\"!\"}}]}\n\n")
45-
fmt.Fprint(w, "data: [DONE]\n\n")
44+
writeResponse(w, "data: {\"choices\":[{\"index\":0,\"delta\":{\"content\":\"hi\"}}]}\n\n")
45+
writeResponse(w, "data: {\"choices\":[{\"index\":0,\"delta\":{\"content\":\"!\"}}]}\n\n")
46+
writeResponse(w, "data: [DONE]\n\n")
4647
}))
4748
defer server.Close()
4849

@@ -135,14 +136,14 @@ func chatTestServer(models []string, onChat func(model string)) *httptest.Server
135136
switch r.URL.Path {
136137
case "/v1/models":
137138
w.Header().Set("Content-Type", "application/json")
138-
fmt.Fprint(w, `{"object":"list","data":[`)
139+
writeResponse(w, `{"object":"list","data":[`)
139140
for i, model := range models {
140141
if i > 0 {
141-
fmt.Fprint(w, ",")
142+
writeResponse(w, ",")
142143
}
143-
fmt.Fprintf(w, `{"id":%q,"object":"model"}`, model)
144+
writeResponsef(w, `{"id":%q,"object":"model"}`, model)
144145
}
145-
fmt.Fprint(w, `]}`)
146+
writeResponse(w, `]}`)
146147
case "/v1/chat/completions":
147148
var body struct {
148149
Model string `json:"model"`
@@ -152,10 +153,20 @@ func chatTestServer(models []string, onChat func(model string)) *httptest.Server
152153
onChat(body.Model)
153154
}
154155
w.Header().Set("Content-Type", "text/event-stream")
155-
fmt.Fprint(w, "data: {\"choices\":[{\"index\":0,\"delta\":{\"content\":\"ok\"}}]}\n\n")
156-
fmt.Fprint(w, "data: [DONE]\n\n")
156+
writeResponse(w, "data: {\"choices\":[{\"index\":0,\"delta\":{\"content\":\"ok\"}}]}\n\n")
157+
writeResponse(w, "data: [DONE]\n\n")
157158
default:
158159
w.WriteHeader(http.StatusNotFound)
159160
}
160161
}))
161162
}
163+
164+
func writeResponse(w io.Writer, text string) {
165+
_, err := fmt.Fprint(w, text)
166+
Expect(err).ToNot(HaveOccurred())
167+
}
168+
169+
func writeResponsef(w io.Writer, format string, args ...any) {
170+
_, err := fmt.Fprintf(w, format, args...)
171+
Expect(err).ToNot(HaveOccurred())
172+
}

core/cli/chat/client.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,9 @@ func (c *localAIChatClient) StreamChat(ctx context.Context, model string, messag
5050
if err != nil {
5151
return "", friendlyChatError(err, model)
5252
}
53-
defer stream.Close()
53+
defer func() {
54+
_ = stream.Close()
55+
}()
5456

5557
var answer strings.Builder
5658
for {

core/cli/chat/terminal.go

Lines changed: 39 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,17 @@ func runTerminalChat(ctx context.Context, session *chatSession, in io.Reader, ou
1212
scanner := bufio.NewScanner(in)
1313
scanner.Buffer(make([]byte, 0, 64*1024), 4*1024*1024)
1414

15-
fmt.Fprintf(out, "LocalAI chat (%s)\n", session.CurrentModel())
16-
fmt.Fprintln(out, "Type /exit to quit, /clear to reset the conversation, /models to list models.")
15+
if err := writeChat(out, "LocalAI chat (%s)\n", session.CurrentModel()); err != nil {
16+
return err
17+
}
18+
if err := writeChat(out, "Type /exit to quit, /clear to reset the conversation, /models to list models.\n"); err != nil {
19+
return err
20+
}
1721

1822
for {
19-
fmt.Fprint(out, "\n> ")
23+
if err := writeChat(out, "\n> "); err != nil {
24+
return err
25+
}
2026
if !scanner.Scan() {
2127
break
2228
}
@@ -26,45 +32,62 @@ func runTerminalChat(ctx context.Context, session *chatSession, in io.Reader, ou
2632
case "":
2733
continue
2834
case "/bye", "/exit", "/quit":
29-
fmt.Fprintln(out, "bye")
30-
return nil
35+
return writeChat(out, "bye\n")
3136
case "/clear":
3237
session.Clear()
33-
fmt.Fprintln(out, "conversation cleared")
38+
if err := writeChat(out, "conversation cleared\n"); err != nil {
39+
return err
40+
}
3441
continue
3542
case "/models":
36-
printChatModels(out, session.Models(), session.CurrentModel())
43+
if err := printChatModels(out, session.Models(), session.CurrentModel()); err != nil {
44+
return err
45+
}
3746
continue
3847
}
3948

4049
if nextModel, ok := strings.CutPrefix(prompt, "/model "); ok {
4150
nextModel = strings.TrimSpace(nextModel)
4251
if nextModel == "" {
43-
fmt.Fprintln(out, "usage: /model <name>")
52+
if err := writeChat(out, "usage: /model <name>\n"); err != nil {
53+
return err
54+
}
4455
continue
4556
}
4657
if err := session.SwitchModel(nextModel); err != nil {
47-
fmt.Fprintln(out, err)
58+
if writeErr := writeChat(out, "%s\n", err); writeErr != nil {
59+
return writeErr
60+
}
4861
continue
4962
}
50-
fmt.Fprintf(out, "switched to %s; conversation cleared\n", session.CurrentModel())
63+
if err := writeChat(out, "switched to %s; conversation cleared\n", session.CurrentModel()); err != nil {
64+
return err
65+
}
5166
continue
5267
}
5368

54-
fmt.Fprint(out, "assistant: ")
69+
if err := writeChat(out, "assistant: "); err != nil {
70+
return err
71+
}
5572
if err := session.Send(ctx, prompt, out); err != nil {
5673
return err
5774
}
58-
fmt.Fprintln(out)
75+
if err := writeChat(out, "\n"); err != nil {
76+
return err
77+
}
5978
}
6079

6180
return scanner.Err()
6281
}
6382

64-
func printChatModels(out io.Writer, models []string, current string) {
83+
func printChatModels(out io.Writer, models []string, current string) error {
6584
if len(models) == 0 {
66-
fmt.Fprintln(out, "no models installed")
67-
return
85+
return writeChat(out, "no models installed\n")
6886
}
69-
fmt.Fprint(out, formatChatModelList(models, current))
87+
return writeChat(out, "%s", formatChatModelList(models, current))
88+
}
89+
90+
func writeChat(out io.Writer, format string, args ...any) error {
91+
_, err := fmt.Fprintf(out, format, args...)
92+
return err
7093
}

0 commit comments

Comments
 (0)