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
This commit is contained in:
Roo Code 2026-04-06 17:15:49 +00:00
parent c3cae397a1
commit 91e27664a2
2 changed files with 140 additions and 22 deletions

View file

@ -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")
})
})
})

View file

@ -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<string> {
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))