From 489af2285e269f84a6f5214a9c4ddbbec3152542 Mon Sep 17 00:00:00 2001 From: Roo Code Date: Mon, 13 Apr 2026 16:25:38 +0000 Subject: [PATCH] fix: disable thinking mode in QwenCodeHandler.completePrompt for prompt enhancement qwen3-coder models are thinking models that return reasoning in a separate reasoning_content field, which can leave message.content empty. The previous fix only stripped tags but did not address the root cause. This change: - Passes enable_thinking: false to the API to disable thinking mode for simple completions (prompt enhancement does not need reasoning) - Falls back to reasoning_content if content is still empty - Strips inline blocks as an additional safety net Closes #12102 --- .../qwen-code-complete-prompt.spec.ts | 215 ++++++++++++++++++ src/api/providers/qwen-code.ts | 25 +- 2 files changed, 237 insertions(+), 3 deletions(-) create mode 100644 src/api/providers/__tests__/qwen-code-complete-prompt.spec.ts diff --git a/src/api/providers/__tests__/qwen-code-complete-prompt.spec.ts b/src/api/providers/__tests__/qwen-code-complete-prompt.spec.ts new file mode 100644 index 0000000000..1280000bfb --- /dev/null +++ b/src/api/providers/__tests__/qwen-code-complete-prompt.spec.ts @@ -0,0 +1,215 @@ +// npx vitest run api/providers/__tests__/qwen-code-complete-prompt.spec.ts + +// Mock filesystem - must come before other imports +vi.mock("node:fs", () => ({ + promises: { + readFile: vi.fn(), + writeFile: vi.fn(), + }, +})) + +const mockCreate = vi.fn() +vi.mock("openai", () => { + return { + __esModule: true, + default: vi.fn().mockImplementation(() => ({ + apiKey: "test-key", + baseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1", + chat: { + completions: { + create: mockCreate, + }, + }, + })), + } +}) + +import { promises as fs } from "node:fs" +import { QwenCodeHandler } from "../qwen-code" +import type { ApiHandlerOptions } from "../../../shared/api" + +describe("QwenCodeHandler completePrompt", () => { + let handler: QwenCodeHandler + let mockOptions: ApiHandlerOptions & { qwenCodeOauthPath?: string } + + const validCredentials = { + access_token: "test-access-token", + refresh_token: "test-refresh-token", + token_type: "Bearer", + expiry_date: Date.now() + 3600000, + resource_url: "https://dashscope.aliyuncs.com/compatible-mode/v1", + } + + beforeEach(() => { + vi.clearAllMocks() + + mockOptions = { + apiModelId: "qwen3-coder-plus", + qwenCodeOauthPath: "/tmp/test-creds.json", + } + + handler = new QwenCodeHandler(mockOptions) + ;(fs.readFile as ReturnType).mockResolvedValue(JSON.stringify(validCredentials)) + }) + + it("should return plain text content as-is", async () => { + mockCreate.mockResolvedValueOnce({ + choices: [ + { + message: { + content: "Here is your enhanced prompt with more details.", + }, + }, + ], + }) + + const result = await handler.completePrompt("Enhance this prompt") + expect(result).toBe("Here is your enhanced prompt with more details.") + }) + + it("should pass enable_thinking: false in the request", async () => { + mockCreate.mockResolvedValueOnce({ + choices: [ + { + message: { + content: "Enhanced text", + }, + }, + ], + }) + + await handler.completePrompt("Enhance this prompt") + + expect(mockCreate).toHaveBeenCalledWith( + expect.objectContaining({ + enable_thinking: false, + }), + ) + }) + + it("should strip blocks from response content", async () => { + mockCreate.mockResolvedValueOnce({ + choices: [ + { + message: { + content: + "Let me analyze this prompt and think about how to enhance it...Here is your enhanced prompt with more details.", + }, + }, + ], + }) + + const result = await handler.completePrompt("Enhance this prompt") + expect(result).toBe("Here is your enhanced prompt with more details.") + }) + + it("should strip multiple blocks from response content", async () => { + mockCreate.mockResolvedValueOnce({ + choices: [ + { + message: { + content: "First thought...Part one. Second thought...Part two.", + }, + }, + ], + }) + + const result = await handler.completePrompt("Enhance this prompt") + expect(result).toBe("Part one. Part two.") + }) + + it("should fall back to reasoning_content when content is empty", async () => { + mockCreate.mockResolvedValueOnce({ + choices: [ + { + message: { + content: "", + reasoning_content: "The actual enhanced prompt text from reasoning.", + }, + }, + ], + }) + + const result = await handler.completePrompt("Enhance this prompt") + expect(result).toBe("The actual enhanced prompt text from reasoning.") + }) + + it("should fall back to reasoning_content when content is null", async () => { + mockCreate.mockResolvedValueOnce({ + choices: [ + { + message: { + content: null, + reasoning_content: "Enhanced prompt from reasoning_content field.", + }, + }, + ], + }) + + const result = await handler.completePrompt("Enhance this prompt") + expect(result).toBe("Enhanced prompt from reasoning_content field.") + }) + + it("should return empty string when both content and reasoning_content are empty", async () => { + mockCreate.mockResolvedValueOnce({ + choices: [ + { + message: { + content: "", + reasoning_content: "", + }, + }, + ], + }) + + const result = await handler.completePrompt("Enhance this prompt") + expect(result).toBe("") + }) + + it("should handle response with only blocks (content becomes empty after stripping)", async () => { + mockCreate.mockResolvedValueOnce({ + choices: [ + { + message: { + content: "Only thinking, no actual content", + }, + }, + ], + }) + + const result = await handler.completePrompt("Enhance this prompt") + expect(result).toBe("") + }) + + it("should handle multiline blocks", async () => { + mockCreate.mockResolvedValueOnce({ + choices: [ + { + message: { + content: + "\nStep 1: Analyze the prompt\nStep 2: Enhance it\nStep 3: Return result\n\nHere is the enhanced prompt.", + }, + }, + ], + }) + + const result = await handler.completePrompt("Enhance this prompt") + expect(result).toBe("Here is the enhanced prompt.") + }) + + it("should prefer content over reasoning_content when content is non-empty", async () => { + mockCreate.mockResolvedValueOnce({ + choices: [ + { + message: { + content: "The actual content response.", + reasoning_content: "Some reasoning that should be ignored.", + }, + }, + ], + }) + + const result = await handler.completePrompt("Enhance this prompt") + expect(result).toBe("The actual content response.") + }) +}) diff --git a/src/api/providers/qwen-code.ts b/src/api/providers/qwen-code.ts index f2a207051e..155e5f6a80 100644 --- a/src/api/providers/qwen-code.ts +++ b/src/api/providers/qwen-code.ts @@ -332,14 +332,33 @@ export class QwenCodeHandler extends BaseProvider implements SingleCompletionHan const client = this.ensureClient() const model = this.getModel() - const requestOptions: OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming = { + // Disable thinking mode for simple completions (e.g. prompt enhancement). + // qwen3-coder models are thinking models that put reasoning in a separate + // `reasoning_content` field, which can leave `content` empty when thinking + // is enabled. Explicitly disabling it ensures content is populated. + const requestOptions: OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming & { + enable_thinking?: boolean + } = { model: model.id, messages: [{ role: "user", content: prompt }], max_completion_tokens: model.info.maxTokens, + enable_thinking: false, } - const response = await this.callApiWithRetry(() => client.chat.completions.create(requestOptions)) + const response = await this.callApiWithRetry(() => client.chat.completions.create(requestOptions as any)) - return response.choices[0]?.message.content || "" + const message = response.choices[0]?.message + let content = message?.content || "" + + // Fallback: if content is empty, check for reasoning_content (in case + // the API still returned thinking content despite enable_thinking: false). + if (!content && message && "reasoning_content" in message) { + content = (message as any).reasoning_content || "" + } + + // Strip any inline ... blocks that may be present. + content = content.replace(/[\s\S]*?<\/think>/g, "").trim() + + return content } }