mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-04 06:12:10 +03:00
fix(sse): flatten structured (array) content in Qwen Web executor (#6927)
* fix(sse): flatten structured (array) content in Qwen Web executor foldMessages did String(m.content), turning OpenAI-style content-part arrays into the literal "[object Object]" prompt. Add contentToText() to extract the text parts. Reported on the support mesh. TDD: red->green regression test tests/unit/qwen-web-content-array-serialization.test.ts * docs(changelog): add fragment for #6927 * fix(stryker): register qwen-web content-array test in tap.testFiles Fast Quality Gates flagged the new coverage for open-sse/executors/qwen-web.ts as missing from stryker.conf.json's tap.testFiles list. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
7ca422d258
commit
2a5f9a5ed7
1
changelog.d/fixes/6927-qwen-web-content-array.md
Normal file
1
changelog.d/fixes/6927-qwen-web-content-array.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(sse): Qwen Web executor no longer sends `[object Object]` when a message uses structured (array) content — the text parts are now flattened (#6927)
|
||||
@@ -255,11 +255,32 @@ export class QwenWebExecutor extends BaseExecutor {
|
||||
};
|
||||
}
|
||||
|
||||
/** Flatten OpenAI-style content (string | Array<{type,text}>) into plain text.
|
||||
* A bare String() on an array of content parts yields "[object Object]" — the
|
||||
* serialization bug reported on the support mesh. */
|
||||
private contentToText(content: unknown): string {
|
||||
if (typeof content === "string") return content;
|
||||
if (Array.isArray(content)) {
|
||||
return content
|
||||
.map((part) => {
|
||||
if (typeof part === "string") return part;
|
||||
if (part && typeof part === "object") {
|
||||
const p = part as { type?: unknown; text?: unknown };
|
||||
if (typeof p.text === "string") return p.text;
|
||||
}
|
||||
return "";
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
}
|
||||
return content == null ? "" : String(content);
|
||||
}
|
||||
|
||||
private foldMessages(messages: Array<{ role: string; content: unknown }>): string {
|
||||
let systemContent = "";
|
||||
let userContent = "";
|
||||
for (const m of messages) {
|
||||
const text = String(m.content ?? "");
|
||||
const text = this.contentToText(m.content);
|
||||
if (m.role === "system") {
|
||||
systemContent += (systemContent ? "\n\n" : "") + text;
|
||||
} else if (m.role === "user") {
|
||||
|
||||
@@ -218,6 +218,7 @@
|
||||
"tests/unit/quota-policy-generalization.test.ts",
|
||||
"tests/unit/quota-pool-log-route.test.ts",
|
||||
"tests/unit/quota-streaming-consumption-usd.test.ts",
|
||||
"tests/unit/qwen-web-content-array-serialization.test.ts",
|
||||
"tests/unit/rate-limit-enhanced.test.ts",
|
||||
"tests/unit/rate-limit-manager.test.ts",
|
||||
"tests/unit/rate-limit-queue-timeout-lockout.test.ts",
|
||||
|
||||
83
tests/unit/qwen-web-content-array-serialization.test.ts
Normal file
83
tests/unit/qwen-web-content-array-serialization.test.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
// Regression: Qwen Web executor folded structured (array) message content with a
|
||||
// bare String(m.content), producing the literal "[object Object]" prompt instead of
|
||||
// the real text (reported on the support mesh: "[[object][object]] serialisation error").
|
||||
// The executor must flatten OpenAI-style content parts into their text before sending.
|
||||
import { describe, it, afterEach } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
const mod = await import("../../open-sse/executors/qwen-web.ts");
|
||||
|
||||
type FetchCall = { url: string; init: { method?: string; body?: string } };
|
||||
const realFetch = globalThis.fetch;
|
||||
|
||||
function sseResponse(events: Array<Record<string, unknown>>): Response {
|
||||
const encoder = new TextEncoder();
|
||||
const stream = new ReadableStream({
|
||||
start(controller) {
|
||||
for (const ev of events) {
|
||||
controller.enqueue(encoder.encode(`data: ${JSON.stringify(ev)}\n\n`));
|
||||
}
|
||||
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
|
||||
controller.close();
|
||||
},
|
||||
});
|
||||
return new Response(stream, {
|
||||
status: 200,
|
||||
headers: { "content-type": "text/event-stream" },
|
||||
});
|
||||
}
|
||||
|
||||
function chatCreatedResponse(id = "chat-arr"): Response {
|
||||
return new Response(JSON.stringify({ success: true, data: { id } }), {
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
globalThis.fetch = realFetch;
|
||||
});
|
||||
|
||||
describe("QwenWebExecutor — structured (array) content serialization", () => {
|
||||
it("flattens OpenAI-style content parts to text (no '[object Object]')", async () => {
|
||||
const calls: FetchCall[] = [];
|
||||
globalThis.fetch = (async (url: string | URL | Request, init: RequestInit = {}) => {
|
||||
calls.push({ url: String(url), init: init as { method?: string; body?: string } });
|
||||
if (String(url).includes("/api/v2/chats/new")) return chatCreatedResponse();
|
||||
return sseResponse([
|
||||
{ choices: [{ delta: { phase: "answer", content: "ok", status: "finished" } }] },
|
||||
]);
|
||||
}) as typeof fetch;
|
||||
|
||||
const executor = new mod.QwenWebExecutor();
|
||||
await executor.execute({
|
||||
model: "qwen3.7-max",
|
||||
body: {
|
||||
messages: [
|
||||
{ role: "system", content: [{ type: "text", text: "You are helpful." }] },
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{ type: "text", text: "First part." },
|
||||
{ type: "text", text: "Second part." },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
stream: false,
|
||||
credentials: { apiKey: "token=jwt-tok; cna=abc" },
|
||||
signal: null,
|
||||
} as unknown as Parameters<typeof executor.execute>[0]);
|
||||
|
||||
const compBody = JSON.parse(calls[1].init.body);
|
||||
const sent = String(compBody.messages[0].content);
|
||||
|
||||
assert.ok(
|
||||
!sent.includes("[object Object]"),
|
||||
`prompt must not contain '[object Object]', got: ${sent}`
|
||||
);
|
||||
assert.ok(sent.includes("First part."), "text of first content part must survive");
|
||||
assert.ok(sent.includes("Second part."), "text of second content part must survive");
|
||||
assert.ok(sent.includes("You are helpful."), "system content part must survive");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user