Update server/chatStream.js
Browse files- server/chatStream.js +12 -5
server/chatStream.js
CHANGED
|
@@ -39,7 +39,7 @@ export async function streamChat(ws, {
|
|
| 39 |
|
| 40 |
const messages = [
|
| 41 |
{ role: "system", content: SYSTEM_PROMPT },
|
| 42 |
-
...history.map(normalizeMessage),
|
| 43 |
{ role: "user", content: userMessage },
|
| 44 |
];
|
| 45 |
|
|
@@ -119,7 +119,7 @@ export async function streamChat(ws, {
|
|
| 119 |
// Follow-up response after tool calls
|
| 120 |
const followUpMessages = [
|
| 121 |
{ role: "system", content: SYSTEM_PROMPT },
|
| 122 |
-
...history.map(normalizeMessage),
|
| 123 |
{ role: "user", content: userMessage },
|
| 124 |
{ role: "assistant", content: assistantText || "", tool_calls: toolCalls },
|
| 125 |
...toolResults,
|
|
@@ -138,11 +138,12 @@ export async function streamChat(ws, {
|
|
| 138 |
|
| 139 |
if (followUp.ok) {
|
| 140 |
const fuReader = followUp.body.getReader();
|
|
|
|
| 141 |
let fuBuffer = "";
|
| 142 |
while (true) {
|
| 143 |
const { done, value } = await fuReader.read();
|
| 144 |
if (done) break;
|
| 145 |
-
fuBuffer +=
|
| 146 |
const fuLines = fuBuffer.split("\n");
|
| 147 |
fuBuffer = fuLines.pop() || "";
|
| 148 |
for (const line of fuLines) {
|
|
@@ -171,11 +172,17 @@ export async function streamChat(ws, {
|
|
| 171 |
}
|
| 172 |
}
|
| 173 |
|
|
|
|
|
|
|
| 174 |
function normalizeMessage(msg) {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 175 |
if (msg.role === "assistant" && msg.tool_calls) {
|
| 176 |
return { role: "assistant", content: "", tool_calls: msg.tool_calls };
|
| 177 |
}
|
| 178 |
-
// Flatten multipart content arrays to text-only for history
|
| 179 |
if (Array.isArray(msg.content)) {
|
| 180 |
const textOnly = msg.content
|
| 181 |
.filter(b => b.type === "text")
|
|
@@ -386,4 +393,4 @@ async function processToolCalls(ws, toolCalls, tools, accessToken, clientId, abo
|
|
| 386 |
}
|
| 387 |
|
| 388 |
return toolResults;
|
| 389 |
-
}
|
|
|
|
| 39 |
|
| 40 |
const messages = [
|
| 41 |
{ role: "system", content: SYSTEM_PROMPT },
|
| 42 |
+
...history.map(normalizeMessage).filter(Boolean),
|
| 43 |
{ role: "user", content: userMessage },
|
| 44 |
];
|
| 45 |
|
|
|
|
| 119 |
// Follow-up response after tool calls
|
| 120 |
const followUpMessages = [
|
| 121 |
{ role: "system", content: SYSTEM_PROMPT },
|
| 122 |
+
...history.map(normalizeMessage).filter(Boolean),
|
| 123 |
{ role: "user", content: userMessage },
|
| 124 |
{ role: "assistant", content: assistantText || "", tool_calls: toolCalls },
|
| 125 |
...toolResults,
|
|
|
|
| 138 |
|
| 139 |
if (followUp.ok) {
|
| 140 |
const fuReader = followUp.body.getReader();
|
| 141 |
+
const fuDecoder = new TextDecoder(); // fresh decoder — reusing the first one corrupts the stream
|
| 142 |
let fuBuffer = "";
|
| 143 |
while (true) {
|
| 144 |
const { done, value } = await fuReader.read();
|
| 145 |
if (done) break;
|
| 146 |
+
fuBuffer += fuDecoder.decode(value, { stream: true });
|
| 147 |
const fuLines = fuBuffer.split("\n");
|
| 148 |
fuBuffer = fuLines.pop() || "";
|
| 149 |
for (const line of fuLines) {
|
|
|
|
| 172 |
}
|
| 173 |
}
|
| 174 |
|
| 175 |
+
const VALID_ROLES = new Set(["system", "user", "assistant", "tool"]);
|
| 176 |
+
|
| 177 |
function normalizeMessage(msg) {
|
| 178 |
+
// Drop asset entries (role: "image"/"video"/"audio") — these are UI-only
|
| 179 |
+
// and sending them to the LLM causes invalid-role rejections / blank responses
|
| 180 |
+
if (!VALID_ROLES.has(msg.role)) return null;
|
| 181 |
+
|
| 182 |
if (msg.role === "assistant" && msg.tool_calls) {
|
| 183 |
return { role: "assistant", content: "", tool_calls: msg.tool_calls };
|
| 184 |
}
|
| 185 |
+
// Flatten multipart content arrays (e.g. image attachments) to text-only for history
|
| 186 |
if (Array.isArray(msg.content)) {
|
| 187 |
const textOnly = msg.content
|
| 188 |
.filter(b => b.type === "text")
|
|
|
|
| 393 |
}
|
| 394 |
|
| 395 |
return toolResults;
|
| 396 |
+
}
|