mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
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:
parent
7adbfec2a4
commit
489af2285e
2 changed files with 237 additions and 3 deletions
215
src/api/providers/__tests__/qwen-code-complete-prompt.spec.ts
Normal file
215
src/api/providers/__tests__/qwen-code-complete-prompt.spec.ts
Normal 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.")
|
||||
})
|
||||
})
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue