fix(qwen): bound video success response (#96604)

(cherry picked from commit cc124d2921)
This commit is contained in:
Alix-007
2026-06-26 01:40:07 +08:00
committed by Dallin Romney
parent 7c1f72cb6f
commit c183cb2911
2 changed files with 80 additions and 1 deletions
@@ -8,6 +8,39 @@ import { describeQwenVideo } from "./media-understanding-provider.js";
installPinnedHostnameTestHooks();
function oversizedJsonResponse(params: { chunkCount: number; chunkSize: number }): {
response: Response;
getReadCount: () => number;
wasCanceled: () => boolean;
} {
const chunk = new Uint8Array(params.chunkSize);
let readCount = 0;
let canceled = false;
return {
response: new Response(
new ReadableStream<Uint8Array>({
pull(controller) {
if (readCount >= params.chunkCount) {
controller.close();
return;
}
readCount += 1;
controller.enqueue(chunk);
},
cancel() {
canceled = true;
},
}),
{
status: 200,
headers: { "Content-Type": "application/json" },
},
),
getReadCount: () => readCount,
wasCanceled: () => canceled,
};
}
describe("describeQwenVideo", () => {
it("builds the expected OpenAI-compatible video payload", async () => {
const { fetchFn, getRequest } = createRequestCaptureJsonFetch({
@@ -74,4 +107,42 @@ describe("describeQwenVideo", () => {
`data:video/mp4;base64,${Buffer.from("video-bytes").toString("base64")}`,
);
});
it("bounds successful Qwen video JSON bodies instead of buffering the whole response", async () => {
const streamed = oversizedJsonResponse({ chunkCount: 64, chunkSize: 1024 * 1024 });
await expect(
describeQwenVideo({
buffer: Buffer.from("video-bytes"),
fileName: "clip.mp4",
mime: "video/mp4",
apiKey: "test-key",
timeoutMs: 1500,
baseUrl: "https://example.com/v1",
fetchFn: async () => streamed.response,
}),
).rejects.toThrow("Qwen video description failed: JSON response exceeds 16777216 bytes");
expect(streamed.getReadCount()).toBeLessThan(64);
expect(streamed.wasCanceled()).toBe(true);
});
it("reports malformed Qwen video JSON with a provider-owned error", async () => {
const response = new Response("not-json{", {
status: 200,
headers: { "Content-Type": "application/json" },
});
await expect(
describeQwenVideo({
buffer: Buffer.from("video-bytes"),
fileName: "clip.mp4",
mime: "video/mp4",
apiKey: "test-key",
timeoutMs: 1500,
baseUrl: "https://example.com/v1",
fetchFn: async () => response,
}),
).rejects.toThrow("Qwen video description failed: malformed JSON response");
});
});
@@ -13,6 +13,7 @@ import {
import {
assertOkOrThrowHttpError,
postJsonRequest,
readProviderJsonResponse,
resolveProviderHttpRequestConfig,
} from "openclaw/plugin-sdk/provider-http";
import { QWEN_STANDARD_GLOBAL_BASE_URL } from "./models.js";
@@ -60,7 +61,14 @@ export async function describeQwenVideo(
try {
await assertOkOrThrowHttpError(res, "Qwen video description failed");
const payload = (await res.json()) as OpenAiCompatibleVideoPayload;
// Read the success body through the shared byte-bounded JSON reader (16 MiB cap +
// stream cancel on overflow) so a hostile or buggy endpoint cannot force the runtime
// to buffer an unbounded body. Malformed JSON keeps the
// `Qwen video description failed: malformed JSON response` wrapping.
const payload = await readProviderJsonResponse<OpenAiCompatibleVideoPayload>(
res,
"Qwen video description failed",
);
const text = coerceOpenAiCompatibleVideoText(payload);
if (!text) {
throw new Error("Qwen video description response missing content");