mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
chatgpt-web: fold history into system message to fix turn concatenation
Reported by user testing in Open WebUI: across three turns
user "test 1" -> "1"
user "test 2" -> "12" (should be "2")
user "test 3. reply only with 3" -> "1123" (should be "3")
The model was literally APPENDING prior assistant outputs into the new
generation instead of producing a fresh response. Root cause: when
sending each prior turn as a separate `assistant`-role entry in
`/backend-api/f/conversation`'s `messages` array, ChatGPT's web API
("action: next") treats those as in-progress messages the model can
continue rather than as completed turns. So the new generation extends
the most recent assistant message.
Fix: don't replay prior turns as separate messages. Instead fold the
full history into the system message as plain text and send only the
current user query as a single new turn. Verified end-to-end:
Turn 1 -> "1"
Turn 2 -> "2"
Turn 3 -> "3"
user "favorite color is teal" / assistant "Got it" / user "what color?"
-> "Teal" (memory still preserved through the system-message channel)
Streaming + multi-turn -> correct, real-time chunks
Updated two unit tests that previously asserted history items showed up
as separate `user`/`assistant` messages in the request — they now check
for the single-user-message + history-in-system-message shape.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -649,28 +649,37 @@ function buildConversationBody(
|
||||
conversationId: string | null,
|
||||
parentMessageId: string
|
||||
): Record<string, unknown> {
|
||||
const messages: ChatGptMessage[] = [];
|
||||
// Critical: do NOT send prior turns as separate `assistant` and `user`
|
||||
// messages in the `messages` array. ChatGPT's web API ("action: next")
|
||||
// treats those as in-progress turns and the model will literally CONTINUE
|
||||
// a prior assistant response in the new generation — observed as
|
||||
// `[1] -> [12] -> [1123]` across three turns.
|
||||
//
|
||||
// Instead, fold all prior history into the system message and send only
|
||||
// the current user message as a single new turn. The model then sees a
|
||||
// single prompt with full context and responds fresh.
|
||||
const systemParts: string[] = [];
|
||||
if (parsed.systemMsg.trim()) {
|
||||
systemParts.push(parsed.systemMsg.trim());
|
||||
}
|
||||
if (parsed.history.length > 0 && !conversationId) {
|
||||
const formatted = parsed.history
|
||||
.map((h) => `${h.role === "assistant" ? "Assistant" : "User"}: ${h.content}`)
|
||||
.join("\n\n");
|
||||
systemParts.push(
|
||||
`Prior conversation (for context — answer only the new user message below):\n\n${formatted}`
|
||||
);
|
||||
}
|
||||
|
||||
if (parsed.systemMsg.trim() && !conversationId) {
|
||||
const messages: ChatGptMessage[] = [];
|
||||
if (systemParts.length > 0 && !conversationId) {
|
||||
messages.push({
|
||||
id: randomUUID(),
|
||||
author: { role: "system" },
|
||||
content: { content_type: "text", parts: [parsed.systemMsg.trim()] },
|
||||
content: { content_type: "text", parts: [systemParts.join("\n\n")] },
|
||||
});
|
||||
}
|
||||
|
||||
// Replay history only on a brand-new conversation; ChatGPT remembers prior turns
|
||||
// server-side once a conversation_id exists.
|
||||
if (!conversationId) {
|
||||
for (const h of parsed.history) {
|
||||
messages.push({
|
||||
id: randomUUID(),
|
||||
author: { role: h.role },
|
||||
content: { content_type: "text", parts: [h.content] },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
messages.push({
|
||||
id: randomUUID(),
|
||||
author: { role: "user" },
|
||||
|
||||
@@ -805,9 +805,15 @@ test("Session continuity: each call starts a fresh conversation (Temporary Chat
|
||||
assert.equal(convIndices.length, 2);
|
||||
const secondBody = JSON.parse(m.calls.bodies[convIndices[1]]);
|
||||
assert.equal(secondBody.conversation_id, null, "should start a fresh conversation");
|
||||
// The full history should be replayed in the messages array.
|
||||
// History is folded into the system message (so the model doesn't try to
|
||||
// continue prior assistant turns); only the latest user message is sent.
|
||||
const userMessages = secondBody.messages.filter((m) => m.author?.role === "user");
|
||||
assert.equal(userMessages.length, 2, "should include First question + Follow-up");
|
||||
assert.equal(userMessages.length, 1, "only the latest user message is in the messages array");
|
||||
assert.equal(userMessages[0].content.parts[0], "Follow-up");
|
||||
const systemMsg = secondBody.messages.find((m) => m.author?.role === "system");
|
||||
assert.ok(systemMsg, "history should be packaged in a system message");
|
||||
assert.match(systemMsg.content.parts[0], /First question/);
|
||||
assert.match(systemMsg.content.parts[0], /Hello, world!/);
|
||||
} finally {
|
||||
m.restore();
|
||||
}
|
||||
@@ -864,9 +870,10 @@ test("Request: payload has correct ChatGPT shape", async () => {
|
||||
assert.equal(body.action, "next");
|
||||
assert.equal(body.model, "gpt-5-3");
|
||||
assert.equal(body.history_and_training_disabled, true);
|
||||
// System + user in messages
|
||||
// System message preserves the user-supplied system prompt; the user
|
||||
// message is the latest query.
|
||||
assert.equal(body.messages[0].author.role, "system");
|
||||
assert.equal(body.messages[0].content.parts[0], "Be concise");
|
||||
assert.match(body.messages[0].content.parts[0], /Be concise/);
|
||||
assert.equal(body.messages[body.messages.length - 1].author.role, "user");
|
||||
assert.equal(body.messages[body.messages.length - 1].content.parts[0], "What is 2+2?");
|
||||
} finally {
|
||||
|
||||
Reference in New Issue
Block a user