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 <think> 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 <think> blocks as an additional safety net

Closes #12102
This commit is contained in:
Roo Code 2026-04-13 16:25:38 +00:00
parent 7adbfec2a4
commit 489af2285e
2 changed files with 237 additions and 3 deletions

View file

@ -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<typeof vi.fn>).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 <think> blocks from response content", async () => {
mockCreate.mockResolvedValueOnce({
choices: [
{
message: {
content:
"<think>Let me analyze this prompt and think about how to enhance it...</think>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 <think> blocks from response content", async () => {
mockCreate.mockResolvedValueOnce({
choices: [
{
message: {
content: "<think>First thought...</think>Part one. <think>Second thought...</think>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 <think> blocks (content becomes empty after stripping)", async () => {
mockCreate.mockResolvedValueOnce({
choices: [
{
message: {
content: "<think>Only thinking, no actual content</think>",
},
},
],
})
const result = await handler.completePrompt("Enhance this prompt")
expect(result).toBe("")
})
it("should handle multiline <think> blocks", async () => {
mockCreate.mockResolvedValueOnce({
choices: [
{
message: {
content:
"<think>\nStep 1: Analyze the prompt\nStep 2: Enhance it\nStep 3: Return result\n</think>\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.")
})
})

View file

@ -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 <think>...</think> blocks that may be present.
content = content.replace(/<think>[\s\S]*?<\/think>/g, "").trim()
return content
}
}