mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-10-04 02:33:34 +00:00
Merge 489af2285e into cb83656718
This commit is contained in:
commit
bdfd101bf9
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 client = this.ensureClient()
|
||||||
const model = this.getModel()
|
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,
|
model: model.id,
|
||||||
messages: [{ role: "user", content: prompt }],
|
messages: [{ role: "user", content: prompt }],
|
||||||
max_completion_tokens: model.info.maxTokens,
|
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