diff --git a/core/http/endpoints/openresponses/responses.go b/core/http/endpoints/openresponses/responses.go index d88c95326..88b0c8c28 100644 --- a/core/http/endpoints/openresponses/responses.go +++ b/core/http/endpoints/openresponses/responses.go @@ -755,7 +755,7 @@ func convertORMessageItem(itemMap map[string]any, cfg *config.ModelConfig) (sche partType, _ := partMap["type"].(string) switch partType { - case "input_text": + case "input_text", "output_text": if text, ok := partMap["text"].(string); ok { textContent += text } diff --git a/core/http/endpoints/openresponses/responses_convert_test.go b/core/http/endpoints/openresponses/responses_convert_test.go index 2a204b4a4..5326f008d 100644 --- a/core/http/endpoints/openresponses/responses_convert_test.go +++ b/core/http/endpoints/openresponses/responses_convert_test.go @@ -1,6 +1,8 @@ package openresponses import ( + "encoding/json" + "github.com/mudler/LocalAI/core/config" "github.com/mudler/LocalAI/core/schema" @@ -50,6 +52,66 @@ var _ = Describe("convertORInputToMessages", func() { Expect(msgs[0].Content).To(Equal("Typed")) }) + Context("replaying serialized response output", func() { + outputItems := func(items ...schema.ORItemField) []any { + data, err := json.Marshal(schema.ORResponseResource{Output: items}) + Expect(err).NotTo(HaveOccurred()) + var response struct { + Output []any `json:"output"` + } + Expect(json.Unmarshal(data, &response)).To(Succeed()) + return response.Output + } + convertRequest := func(input []any) []schema.Message { + data, err := json.Marshal(schema.OpenResponsesRequest{Input: input}) + Expect(err).NotTo(HaveOccurred()) + var request schema.OpenResponsesRequest + Expect(json.Unmarshal(data, &request)).To(Succeed()) + msgs, err := convertORInputToMessages(request.Input, cfg) + Expect(err).NotTo(HaveOccurred()) + return msgs + } + assistant := func(parts ...string) schema.ORItemField { + var content []schema.ORContentPart + for _, text := range parts { + content = append(content, schema.ORContentPart{Type: "output_text", Text: text}) + } + return schema.ORItemField{Type: "message", Role: "assistant", Content: content} + } + user := func(text string) any { + return map[string]any{"role": "user", "content": []any{ + map[string]any{"type": "input_text", "text": text}, + }} + } + + It("preserves assistant output_text in both content fields", func() { + msgs := convertRequest(outputItems(assistant("Hello again"))) + Expect(msgs).To(HaveLen(1)) + Expect(msgs[0].Role).To(Equal("assistant")) + Expect(msgs[0].Content).To(Equal("Hello again")) + Expect(msgs[0].StringContent).To(Equal("Hello again")) + }) + + It("joins text parts and contiguous assistant messages across user input_text boundaries", func() { + input := []any{user("First question")} + input = append(input, outputItems(assistant("First ", "answer"), assistant("More detail"))...) + input = append(input, user("Next question")) + input = append(input, outputItems(assistant("Second answer"))...) + msgs := convertRequest(input) + Expect(msgs).To(HaveLen(4)) + for i, expected := range []struct{ role, text string }{ + {"user", "First question"}, + {"assistant", "First answer\nMore detail"}, + {"user", "Next question"}, + {"assistant", "Second answer"}, + } { + Expect(msgs[i].Role).To(Equal(expected.role)) + Expect(msgs[i].Content).To(Equal(expected.text)) + Expect(msgs[i].StringContent).To(Equal(expected.text)) + } + }) + }) + It("does not treat a non-message item (no content key) as a message", func() { // An item with neither a known type nor a {role, content} shape must // keep falling through unchanged — no behaviour change for such inputs. diff --git a/docs/content/features/text-generation.md b/docs/content/features/text-generation.md index b133f75e4..fecd61eb0 100644 --- a/docs/content/features/text-generation.md +++ b/docs/content/features/text-generation.md @@ -242,6 +242,37 @@ curl http://localhost:8080/v1/responses \ }' ``` +#### Reuse assistant messages + +When you manage conversation history in your client, append assistant messages from a response's `output` array to the next request's `input`. +Keep their content type as `output_text`. Use `input_text` for new user messages. +Include earlier conversation items that the model still needs. + +For example, submit this body to `POST /v1/responses`: + +```json +{ + "model": "ggml-koala-7b-model-q4_0-r2.bin", + "input": [ + { + "type": "message", + "role": "user", + "content": [{"type": "input_text", "text": "What is the capital of France?"}] + }, + { + "type": "message", + "role": "assistant", + "content": [{"type": "output_text", "text": "Paris is the capital of France."}] + }, + { + "type": "message", + "role": "user", + "content": [{"type": "input_text", "text": "Which river flows through it?"}] + } + ] +} +``` + #### WebSocket Responses Connect to `ws://localhost:8080/v1/responses` (or `wss://` when TLS is