fix(responses): preserve replayed output_text

Accept response output text when clients replay it as request input.
Keep assistant history in both prompt content fields and preserve user
message boundaries when merging adjacent assistant messages.

Assisted-by: Codex:GPT-6
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
(cherry picked from commit 1cc5336aaa)
This commit is contained in:
localai-org-maint-bot authored and Ettore Di Giacinto committed 2026-10-25 08:43:05 +00:00
1 parent c0080c7a43
commit 498d18d485
3 files changed
+94 -1

No files matched your search

@@ -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
}
@@ -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.
+31
View File
@@ -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