Skip to content

Commit 16ea031

Browse files
committed
fix: strip thinking blocks from qwen-code completePrompt responses
qwen3-coder models include <think>...</think> blocks in their responses. The completePrompt() method (used for prompt enhancement) was returning these blocks as-is, causing the enhanced prompt to either be empty (triggering "Failed to enhance prompt" toast) or contain raw thinking markup. This strips <think> blocks from the response content before returning, consistent with how createMessage() already handles them for streaming. Closes #12102
1 parent 7adbfec commit 16ea031

2 files changed

Lines changed: 150 additions & 1 deletion

File tree

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
// npx vitest run api/providers/__tests__/qwen-code-complete-prompt.spec.ts
2+
3+
// Mock filesystem - must come before other imports
4+
vi.mock("node:fs", () => ({
5+
promises: {
6+
readFile: vi.fn(),
7+
writeFile: vi.fn(),
8+
},
9+
}))
10+
11+
const mockCreate = vi.fn()
12+
vi.mock("openai", () => {
13+
return {
14+
__esModule: true,
15+
default: vi.fn().mockImplementation(() => ({
16+
apiKey: "test-key",
17+
baseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1",
18+
chat: {
19+
completions: {
20+
create: mockCreate,
21+
},
22+
},
23+
})),
24+
}
25+
})
26+
27+
import { promises as fs } from "node:fs"
28+
import { QwenCodeHandler } from "../qwen-code"
29+
import type { ApiHandlerOptions } from "../../../shared/api"
30+
31+
describe("QwenCodeHandler completePrompt", () => {
32+
let handler: QwenCodeHandler
33+
let mockOptions: ApiHandlerOptions & { qwenCodeOauthPath?: string }
34+
35+
const validCredentials = {
36+
access_token: "test-access-token",
37+
refresh_token: "test-refresh-token",
38+
token_type: "Bearer",
39+
expiry_date: Date.now() + 3600000,
40+
resource_url: "https://dashscope.aliyuncs.com/compatible-mode/v1",
41+
}
42+
43+
beforeEach(() => {
44+
vi.clearAllMocks()
45+
46+
mockOptions = {
47+
apiModelId: "qwen3-coder-plus",
48+
qwenCodeOauthPath: "/tmp/test-creds.json",
49+
}
50+
51+
handler = new QwenCodeHandler(mockOptions)
52+
;(fs.readFile as ReturnType<typeof vi.fn>).mockResolvedValue(JSON.stringify(validCredentials))
53+
})
54+
55+
it("should return plain text content as-is", async () => {
56+
mockCreate.mockResolvedValueOnce({
57+
choices: [
58+
{
59+
message: {
60+
content: "Here is your enhanced prompt with more details.",
61+
},
62+
},
63+
],
64+
})
65+
66+
const result = await handler.completePrompt("Enhance this prompt")
67+
expect(result).toBe("Here is your enhanced prompt with more details.")
68+
})
69+
70+
it("should strip <think> blocks from response content", async () => {
71+
mockCreate.mockResolvedValueOnce({
72+
choices: [
73+
{
74+
message: {
75+
content:
76+
"<think>Let me analyze this prompt and think about how to enhance it...</think>Here is your enhanced prompt with more details.",
77+
},
78+
},
79+
],
80+
})
81+
82+
const result = await handler.completePrompt("Enhance this prompt")
83+
expect(result).toBe("Here is your enhanced prompt with more details.")
84+
})
85+
86+
it("should strip multiple <think> blocks from response content", async () => {
87+
mockCreate.mockResolvedValueOnce({
88+
choices: [
89+
{
90+
message: {
91+
content: "<think>First thought...</think>Part one. <think>Second thought...</think>Part two.",
92+
},
93+
},
94+
],
95+
})
96+
97+
const result = await handler.completePrompt("Enhance this prompt")
98+
expect(result).toBe("Part one. Part two.")
99+
})
100+
101+
it("should handle multiline <think> blocks", async () => {
102+
mockCreate.mockResolvedValueOnce({
103+
choices: [
104+
{
105+
message: {
106+
content:
107+
"<think>\nLet me think about this.\nI need to consider multiple things.\n</think>\nThe enhanced prompt.",
108+
},
109+
},
110+
],
111+
})
112+
113+
const result = await handler.completePrompt("Enhance this prompt")
114+
expect(result).toBe("The enhanced prompt.")
115+
})
116+
117+
it("should return empty string when content is only a think block", async () => {
118+
mockCreate.mockResolvedValueOnce({
119+
choices: [
120+
{
121+
message: {
122+
content: "<think>Only thinking, no actual content.</think>",
123+
},
124+
},
125+
],
126+
})
127+
128+
const result = await handler.completePrompt("Enhance this prompt")
129+
expect(result).toBe("")
130+
})
131+
132+
it("should return empty string when message content is null", async () => {
133+
mockCreate.mockResolvedValueOnce({
134+
choices: [
135+
{
136+
message: {
137+
content: null,
138+
},
139+
},
140+
],
141+
})
142+
143+
const result = await handler.completePrompt("Enhance this prompt")
144+
expect(result).toBe("")
145+
})
146+
})

src/api/providers/qwen-code.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,9 @@ export class QwenCodeHandler extends BaseProvider implements SingleCompletionHan
340340

341341
const response = await this.callApiWithRetry(() => client.chat.completions.create(requestOptions))
342342

343-
return response.choices[0]?.message.content || ""
343+
const content = response.choices[0]?.message.content || ""
344+
345+
// Strip <think>...</think> blocks that qwen3-coder thinking models include
346+
return content.replace(/<think>[\s\S]*?<\/think>/g, "").trim()
344347
}
345348
}

0 commit comments

Comments
 (0)