From 91e27664a2b7eb09d3c2b5c7e63fc8859f142afd Mon Sep 17 00:00:00 2001 From: Roo Code Date: Mon, 6 Apr 2026 17:15:49 +0000 Subject: [PATCH] fix: use DashScope-compatible parameters in Qwen Code provider to fix 400 errors Replace OpenAI-specific parameters that DashScope does not support: - max_completion_tokens -> max_tokens - Remove parallel_tool_calls (unsupported) - Remove stream_options (unsupported) - Override convertToolsForOpenAI to skip strict mode (unsupported) Fixes #12067 --- .../__tests__/qwen-code-native-tools.spec.ts | 129 +++++++++++++++--- src/api/providers/qwen-code.ts | 33 ++++- 2 files changed, 140 insertions(+), 22 deletions(-) diff --git a/src/api/providers/__tests__/qwen-code-native-tools.spec.ts b/src/api/providers/__tests__/qwen-code-native-tools.spec.ts index 3b470ce461e..687809282bd 100644 --- a/src/api/providers/__tests__/qwen-code-native-tools.spec.ts +++ b/src/api/providers/__tests__/qwen-code-native-tools.spec.ts @@ -89,19 +89,19 @@ describe("QwenCodeHandler Native Tools", () => { }) await stream.next() - expect(mockCreate).toHaveBeenCalledWith( - expect.objectContaining({ - tools: expect.arrayContaining([ - expect.objectContaining({ - type: "function", - function: expect.objectContaining({ - name: "test_tool", - }), + const callArgs = mockCreate.mock.calls[0][0] + expect(callArgs.tools).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + type: "function", + function: expect.objectContaining({ + name: "test_tool", }), - ]), - parallel_tool_calls: true, - }), + }), + ]), ) + // DashScope does not support parallel_tool_calls + expect(callArgs).not.toHaveProperty("parallel_tool_calls") }) it("should include tool_choice when provided", async () => { @@ -145,7 +145,8 @@ describe("QwenCodeHandler Native Tools", () => { const callArgs = mockCreate.mock.calls[mockCreate.mock.calls.length - 1][0] expect(callArgs).toHaveProperty("tools") expect(callArgs).toHaveProperty("tool_choice") - expect(callArgs).toHaveProperty("parallel_tool_calls", true) + // DashScope does not support parallel_tool_calls + expect(callArgs).not.toHaveProperty("parallel_tool_calls") }) it("should yield tool_call_partial chunks during streaming", async () => { @@ -215,7 +216,7 @@ describe("QwenCodeHandler Native Tools", () => { }) }) - it("should set parallel_tool_calls based on metadata", async () => { + it("should not include parallel_tool_calls even when metadata provides it (DashScope unsupported)", async () => { mockCreate.mockImplementationOnce(() => ({ [Symbol.asyncIterator]: async function* () { yield { @@ -231,11 +232,9 @@ describe("QwenCodeHandler Native Tools", () => { }) await stream.next() - expect(mockCreate).toHaveBeenCalledWith( - expect.objectContaining({ - parallel_tool_calls: true, - }), - ) + const callArgs = mockCreate.mock.calls[0][0] + // DashScope does not support parallel_tool_calls - should never be sent + expect(callArgs).not.toHaveProperty("parallel_tool_calls") }) it("should yield tool_call_end events when finish_reason is tool_calls", async () => { @@ -370,4 +369,98 @@ describe("QwenCodeHandler Native Tools", () => { expect(endChunks).toHaveLength(1) }) }) + + describe("DashScope API Compatibility", () => { + it("should use max_tokens instead of max_completion_tokens", async () => { + mockCreate.mockImplementationOnce(() => ({ + [Symbol.asyncIterator]: async function* () { + yield { + choices: [{ delta: { content: "Test response" } }], + } + }, + })) + + const stream = handler.createMessage("test prompt", [], { + taskId: "test-task-id", + }) + await stream.next() + + const callArgs = mockCreate.mock.calls[0][0] + expect(callArgs).toHaveProperty("max_tokens") + expect(callArgs).not.toHaveProperty("max_completion_tokens") + }) + + it("should not include stream_options", async () => { + mockCreate.mockImplementationOnce(() => ({ + [Symbol.asyncIterator]: async function* () { + yield { + choices: [{ delta: { content: "Test response" } }], + } + }, + })) + + const stream = handler.createMessage("test prompt", [], { + taskId: "test-task-id", + }) + await stream.next() + + const callArgs = mockCreate.mock.calls[0][0] + expect(callArgs).not.toHaveProperty("stream_options") + }) + + it("should not include parallel_tool_calls", async () => { + mockCreate.mockImplementationOnce(() => ({ + [Symbol.asyncIterator]: async function* () { + yield { + choices: [{ delta: { content: "Test response" } }], + } + }, + })) + + const stream = handler.createMessage("test prompt", [], { + taskId: "test-task-id", + tools: testTools, + }) + await stream.next() + + const callArgs = mockCreate.mock.calls[0][0] + expect(callArgs).not.toHaveProperty("parallel_tool_calls") + }) + + it("should not set strict: true on tool definitions", async () => { + mockCreate.mockImplementationOnce(() => ({ + [Symbol.asyncIterator]: async function* () { + yield { + choices: [{ delta: { content: "Test response" } }], + } + }, + })) + + const stream = handler.createMessage("test prompt", [], { + taskId: "test-task-id", + tools: testTools, + }) + await stream.next() + + const callArgs = mockCreate.mock.calls[0][0] + const tools = callArgs.tools + expect(tools).toBeDefined() + for (const tool of tools) { + // DashScope does not support strict mode + expect(tool.function).not.toHaveProperty("strict") + } + }) + + it("should use max_tokens in completePrompt", async () => { + mockCreate.mockImplementationOnce(() => ({ + choices: [{ message: { content: "response" } }], + })) + + await handler.completePrompt("test prompt") + + const callArgs = mockCreate.mock.calls[0][0] + expect(callArgs).toHaveProperty("max_tokens") + expect(callArgs).not.toHaveProperty("max_completion_tokens") + }) + }) }) diff --git a/src/api/providers/qwen-code.ts b/src/api/providers/qwen-code.ts index 18d09a59f3b..205505084a8 100644 --- a/src/api/providers/qwen-code.ts +++ b/src/api/providers/qwen-code.ts @@ -219,16 +219,19 @@ export class QwenCodeHandler extends BaseProvider implements SingleCompletionHan const convertedMessages = [systemMessage, ...convertToOpenAiMessages(messages)] + // DashScope's OpenAI-compatible API does not support several OpenAI-specific + // parameters. Using them causes a 400 Bad Request error. Specifically: + // - max_completion_tokens -> use max_tokens instead + // - parallel_tool_calls -> not supported + // - stream_options -> not supported const requestOptions: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = { model: model.id, temperature: 0, messages: convertedMessages, stream: true, - stream_options: { include_usage: true }, - max_completion_tokens: model.info.maxTokens, + max_tokens: model.info.maxTokens, tools: this.convertToolsForOpenAI(metadata?.tools), tool_choice: metadata?.tool_choice, - parallel_tool_calls: metadata?.parallelToolCalls ?? true, } const stream = await this.callApiWithRetry(() => client.chat.completions.create(requestOptions)) @@ -321,6 +324,28 @@ export class QwenCodeHandler extends BaseProvider implements SingleCompletionHan return { id, info } } + /** + * Override to skip strict mode for DashScope compatibility. + * DashScope's OpenAI-compatible API does not support OpenAI's strict mode + * on tool definitions (strict: true, additionalProperties: false enforcement). + * Sending these causes a 400 Bad Request error. + */ + protected override convertToolsForOpenAI(tools: any[] | undefined): any[] | undefined { + if (!tools) { + return undefined + } + + return tools + .filter((tool) => tool.type === "function") + .map((tool) => ({ + ...tool, + function: { + ...tool.function, + // Do not set strict: true - DashScope does not support it + }, + })) + } + async completePrompt(prompt: string): Promise { await this.ensureAuthenticated() const client = this.ensureClient() @@ -329,7 +354,7 @@ export class QwenCodeHandler extends BaseProvider implements SingleCompletionHan const requestOptions: OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming = { model: model.id, messages: [{ role: "user", content: prompt }], - max_completion_tokens: model.info.maxTokens, + max_tokens: model.info.maxTokens, } const response = await this.callApiWithRetry(() => client.chat.completions.create(requestOptions))