mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-02 05:12:11 +03:00
* 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>
84 lines
3.0 KiB
TypeScript
84 lines
3.0 KiB
TypeScript
// 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");
|
|
});
|
|
});
|