Repository: ollama/ollama
Pull request: send empty messages on last chat response

Send an empty message on the last chat response rather than omitting it. This makes the chat API match the generate API.

As of this change...

`/generate`
```
curl http://localhost:11434/api/generate -d '{
    "model": "mistral"
}'

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Thu, 14 Dec 2023 22:42:13 GMT
Content-Length: 88
Connection: close

{
  "model": "mistral",
  "created_at": "2023-12-14T22:42:13.365069Z",
  "response": "",
  "done": true
}
```
```
curl http://localhost:11434/api/generate -d '{
    "model": "mistral",
    "prompt": "reply with a single word"
}'

HTTP/1.1 200 OK
Content-Type: application/x-ndjson
Date: Thu, 14 Dec 2023 22:43:05 GMT
Connection: close
Transfer-Encoding: chunked

{"model":"mistral","created_at":"2023-12-14T22:43:05.60168Z","response":" Okay","done":false}
{"model":"mistral","created_at":"2023-12-14T22:43:05.616406Z","response":".","done":false}
{"model":"mistral","created_at":"2023-12-14T22:43:05.631163Z","response":"","done":true,"context":[733,16289,28793,28705,10071,395,264,2692,1707,733,28748,16289,28793,19811,28723],"total_duration":414743625,"load_duration":760500,"prompt_eval_count":14,"prompt_eval_duration":393924000,"eval_count":2,"eval_duration":14727000}
```

`/chat`
```
curl http://localhost:11434/api/chat -d '{
    "model": "mistral"
}'

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Thu, 14 Dec 2023 22:41:56 GMT
Content-Length: 132
Connection: close

{
  "model": "mistral",
  "created_at": "2023-12-14T22:41:56.540246Z",
  "message": {
    "role": "assistant",
    "content": "",
    "images": null
  },
  "done": true
}
```
```
curl http://localhost:11434/api/chat -d '{
    "model": "mistral",
    "messages": [
        {
            "role": "user",
            "content": "reply with one word"
        }
    ]
}'
HTTP/1.1 200 OK
Content-Type: application/x-ndjson
Date: Thu, 14 Dec 2023 22:38:36 GMT
Connection: close
Transfer-Encoding: chunked

{"model":"mistral","created_at":"2023-12-14T22:38:36.625168Z","message":{"role":"assistant","content":" Okay","images":null},"done":false}
{"model":"mistral","created_at":"2023-12-14T22:38:36.639622Z","message":{"role":"assistant","content":".","images":null},"done":false}
{"model":"mistral","created_at":"2023-12-14T22:38:36.654161Z","message":{"role":"assistant","content":"","images":null},"done":true,"total_duration":417861167,"load_duration":927584,"prompt_eval_count":13,"prompt_eval_duration":396099000,"eval_count":2,"eval_duration":14502000}
```

Changed files:
filename: api/types.go
patch: @@ -59,13 +59,13 @@ type ChatRequest struct {
 type Message struct {
 	Role    string      `json:"role"` // one of ["system", "user", "assistant"]
 	Content string      `json:"content"`
-	Images  []ImageData `json:"images, omitempty"`
+	Images  []ImageData `json:"images,omitempty"`
 }
 
 type ChatResponse struct {
 	Model     string    `json:"model"`
 	CreatedAt time.Time `json:"created_at"`
-	Message   *Message  `json:"message,omitempty"`
+	Message   Message   `json:"message"`
 
 	Done bool `json:"done"`
 

filename: server/routes.go
patch: @@ -1013,7 +1013,7 @@ func ChatHandler(c *gin.Context) {
 
 	// an empty request loads the model
 	if len(req.Messages) == 0 {
-		c.JSON(http.StatusOK, api.ChatResponse{CreatedAt: time.Now().UTC(), Model: req.Model, Done: true})
+		c.JSON(http.StatusOK, api.ChatResponse{CreatedAt: time.Now().UTC(), Model: req.Model, Done: true, Message: api.Message{Role: "assistant"}})
 		return
 	}
 
@@ -1038,6 +1038,7 @@ func ChatHandler(c *gin.Context) {
 			resp := api.ChatResponse{
 				Model:     req.Model,
 				CreatedAt: time.Now().UTC(),
+				Message:   api.Message{Role: "assistant", Content: r.Content},
 				Done:      r.Done,
 				Metrics: api.Metrics{
 					PromptEvalCount:    r.PromptEvalCount,
@@ -1050,8 +1051,6 @@ func ChatHandler(c *gin.Context) {
 			if r.Done {
 				resp.TotalDuration = time.Since(checkpointStart)
 				resp.LoadDuration = checkpointLoaded.Sub(checkpointStart)
-			} else {
-				resp.Message = &api.Message{Role: "assistant", Content: r.Content}
 			}
 
 			ch <- resp
@@ -1075,10 +1074,7 @@ func ChatHandler(c *gin.Context) {
 		for resp := range ch {
 			switch r := resp.(type) {
 			case api.ChatResponse:
-				if r.Message != nil {
-					sb.WriteString(r.Message.Content)
-				}
-
+				sb.WriteString(r.Message.Content)
 				final = r
 			case gin.H:
 				if errorMsg, ok := r["error"].(string); ok {
@@ -1094,7 +1090,7 @@ func ChatHandler(c *gin.Context) {
 			}
 		}
 
-		final.Message = &api.Message{Role: "assistant", Content: sb.String()}
+		final.Message = api.Message{Role: "assistant", Content: sb.String()}
 		c.JSON(http.StatusOK, final)
 		return
 	}