Skip to content

Commit 97281d8

Browse files
XuZehan-iCenterXuZehan-iCenter
authored andcommitted
fix(media-understanding): place Qwen/DashScope image prompts in user content
DashScope vision models (qwen3.7-max, qwen3.7-plus) return 400 "Unexpected item type in content" when the image request sends the prompt in a system message and only image blocks in the user message. Add DashScope endpoint detection to shouldPlaceImagePromptInUserContent so the prompt text is placed in the user content alongside the image, matching how GitHub Copilot and OpenRouter routes already work. Fixes #92688
1 parent ca2410a commit 97281d8

2 files changed

Lines changed: 70 additions & 0 deletions

File tree

src/media-understanding/image.test.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -884,6 +884,59 @@ describe("describeImageWithModel", () => {
884884
]);
885885
});
886886

887+
it("places DashScope/Qwen image prompts in user content before images", async () => {
888+
discoverModelsMock.mockReturnValue({
889+
find: vi.fn(() => ({
890+
api: "openai-completions",
891+
provider: "qwen",
892+
id: "qwen3.7-max",
893+
input: ["text", "image"],
894+
baseUrl: "https://dashscope.aliyuncs.com/compatible-mode/v1",
895+
})),
896+
});
897+
completeMock.mockResolvedValue({
898+
role: "assistant",
899+
api: "openai-completions",
900+
provider: "qwen",
901+
model: "qwen3.7-max",
902+
stopReason: "stop",
903+
timestamp: Date.now(),
904+
content: [{ type: "text", text: "qwen dashscope ok" }],
905+
});
906+
907+
const result = await describeImageWithModel({
908+
cfg: {},
909+
agentDir: "/tmp/openclaw-agent",
910+
provider: "qwen",
911+
model: "qwen3.7-max",
912+
buffer: Buffer.from("png-bytes"),
913+
fileName: "image.png",
914+
mime: "image/png",
915+
prompt: "Describe the image.",
916+
timeoutMs: 1000,
917+
});
918+
919+
expect(result).toEqual({
920+
text: "qwen dashscope ok",
921+
model: "qwen3.7-max",
922+
});
923+
const firstCall = requireFirstMockCall(completeMock, "DashScope image completion");
924+
const [, context] = firstCall;
925+
expect(context.systemPrompt).toBeUndefined();
926+
const userMessage = context.messages[0];
927+
if (!userMessage) {
928+
throw new Error("expected DashScope image completion user message");
929+
}
930+
expect(userMessage.content).toEqual([
931+
{ type: "text", text: "Describe the image." },
932+
{
933+
type: "image",
934+
data: Buffer.from("png-bytes").toString("base64"),
935+
mimeType: "image/png",
936+
},
937+
]);
938+
});
939+
887940
it.each([
888941
{
889942
name: "direct OpenAI Responses baseUrl",

src/media-understanding/image.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,17 @@ function buildImageContext(
281281
};
282282
}
283283

284+
function isDashScopeEndpoint(model: Model): boolean {
285+
const provider = model.provider?.toLowerCase() ?? "";
286+
const baseUrl = model.baseUrl?.toLowerCase() ?? "";
287+
return (
288+
provider.includes("dashscope") ||
289+
provider === "qwen" ||
290+
provider === "qwen-dashscope" ||
291+
baseUrl.includes("dashscope.aliyuncs.com")
292+
);
293+
}
294+
284295
function shouldPlaceImagePromptInUserContent(model: Model): boolean {
285296
// GitHub Copilot models (including Gemini 3.1 Pro Preview) require the
286297
// prompt text to be in the user message alongside the image. Placing it
@@ -289,6 +300,12 @@ function shouldPlaceImagePromptInUserContent(model: Model): boolean {
289300
if (model.provider === "github-copilot") {
290301
return true;
291302
}
303+
// DashScope/Qwen vision models return 400 "Unexpected item type in content"
304+
// when the user message contains only image blocks with no text. The prompt
305+
// must be placed in the user content alongside the image.
306+
if (isDashScopeEndpoint(model)) {
307+
return true;
308+
}
292309
const capabilities = resolveProviderRequestCapabilities({
293310
provider: model.provider,
294311
api: model.api,

0 commit comments

Comments
 (0)