mirror of
https://github.com/mudler/LocalAI.git
synced 2026-10-28 00:59:28 -04:00
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:
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.
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user