Skip to content
This repository was archived by the owner on May 15, 2026. It is now read-only.

Commit 91e2766

Browse files
committed
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
1 parent c3cae39 commit 91e2766

2 files changed

Lines changed: 140 additions & 22 deletions

File tree

src/api/providers/__tests__/qwen-code-native-tools.spec.ts

Lines changed: 111 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -89,19 +89,19 @@ describe("QwenCodeHandler Native Tools", () => {
8989
})
9090
await stream.next()
9191

92-
expect(mockCreate).toHaveBeenCalledWith(
93-
expect.objectContaining({
94-
tools: expect.arrayContaining([
95-
expect.objectContaining({
96-
type: "function",
97-
function: expect.objectContaining({
98-
name: "test_tool",
99-
}),
92+
const callArgs = mockCreate.mock.calls[0][0]
93+
expect(callArgs.tools).toEqual(
94+
expect.arrayContaining([
95+
expect.objectContaining({
96+
type: "function",
97+
function: expect.objectContaining({
98+
name: "test_tool",
10099
}),
101-
]),
102-
parallel_tool_calls: true,
103-
}),
100+
}),
101+
]),
104102
)
103+
// DashScope does not support parallel_tool_calls
104+
expect(callArgs).not.toHaveProperty("parallel_tool_calls")
105105
})
106106

107107
it("should include tool_choice when provided", async () => {
@@ -145,7 +145,8 @@ describe("QwenCodeHandler Native Tools", () => {
145145
const callArgs = mockCreate.mock.calls[mockCreate.mock.calls.length - 1][0]
146146
expect(callArgs).toHaveProperty("tools")
147147
expect(callArgs).toHaveProperty("tool_choice")
148-
expect(callArgs).toHaveProperty("parallel_tool_calls", true)
148+
// DashScope does not support parallel_tool_calls
149+
expect(callArgs).not.toHaveProperty("parallel_tool_calls")
149150
})
150151

151152
it("should yield tool_call_partial chunks during streaming", async () => {
@@ -215,7 +216,7 @@ describe("QwenCodeHandler Native Tools", () => {
215216
})
216217
})
217218

218-
it("should set parallel_tool_calls based on metadata", async () => {
219+
it("should not include parallel_tool_calls even when metadata provides it (DashScope unsupported)", async () => {
219220
mockCreate.mockImplementationOnce(() => ({
220221
[Symbol.asyncIterator]: async function* () {
221222
yield {
@@ -231,11 +232,9 @@ describe("QwenCodeHandler Native Tools", () => {
231232
})
232233
await stream.next()
233234

234-
expect(mockCreate).toHaveBeenCalledWith(
235-
expect.objectContaining({
236-
parallel_tool_calls: true,
237-
}),
238-
)
235+
const callArgs = mockCreate.mock.calls[0][0]
236+
// DashScope does not support parallel_tool_calls - should never be sent
237+
expect(callArgs).not.toHaveProperty("parallel_tool_calls")
239238
})
240239

241240
it("should yield tool_call_end events when finish_reason is tool_calls", async () => {
@@ -370,4 +369,98 @@ describe("QwenCodeHandler Native Tools", () => {
370369
expect(endChunks).toHaveLength(1)
371370
})
372371
})
372+
373+
describe("DashScope API Compatibility", () => {
374+
it("should use max_tokens instead of max_completion_tokens", async () => {
375+
mockCreate.mockImplementationOnce(() => ({
376+
[Symbol.asyncIterator]: async function* () {
377+
yield {
378+
choices: [{ delta: { content: "Test response" } }],
379+
}
380+
},
381+
}))
382+
383+
const stream = handler.createMessage("test prompt", [], {
384+
taskId: "test-task-id",
385+
})
386+
await stream.next()
387+
388+
const callArgs = mockCreate.mock.calls[0][0]
389+
expect(callArgs).toHaveProperty("max_tokens")
390+
expect(callArgs).not.toHaveProperty("max_completion_tokens")
391+
})
392+
393+
it("should not include stream_options", async () => {
394+
mockCreate.mockImplementationOnce(() => ({
395+
[Symbol.asyncIterator]: async function* () {
396+
yield {
397+
choices: [{ delta: { content: "Test response" } }],
398+
}
399+
},
400+
}))
401+
402+
const stream = handler.createMessage("test prompt", [], {
403+
taskId: "test-task-id",
404+
})
405+
await stream.next()
406+
407+
const callArgs = mockCreate.mock.calls[0][0]
408+
expect(callArgs).not.toHaveProperty("stream_options")
409+
})
410+
411+
it("should not include parallel_tool_calls", async () => {
412+
mockCreate.mockImplementationOnce(() => ({
413+
[Symbol.asyncIterator]: async function* () {
414+
yield {
415+
choices: [{ delta: { content: "Test response" } }],
416+
}
417+
},
418+
}))
419+
420+
const stream = handler.createMessage("test prompt", [], {
421+
taskId: "test-task-id",
422+
tools: testTools,
423+
})
424+
await stream.next()
425+
426+
const callArgs = mockCreate.mock.calls[0][0]
427+
expect(callArgs).not.toHaveProperty("parallel_tool_calls")
428+
})
429+
430+
it("should not set strict: true on tool definitions", async () => {
431+
mockCreate.mockImplementationOnce(() => ({
432+
[Symbol.asyncIterator]: async function* () {
433+
yield {
434+
choices: [{ delta: { content: "Test response" } }],
435+
}
436+
},
437+
}))
438+
439+
const stream = handler.createMessage("test prompt", [], {
440+
taskId: "test-task-id",
441+
tools: testTools,
442+
})
443+
await stream.next()
444+
445+
const callArgs = mockCreate.mock.calls[0][0]
446+
const tools = callArgs.tools
447+
expect(tools).toBeDefined()
448+
for (const tool of tools) {
449+
// DashScope does not support strict mode
450+
expect(tool.function).not.toHaveProperty("strict")
451+
}
452+
})
453+
454+
it("should use max_tokens in completePrompt", async () => {
455+
mockCreate.mockImplementationOnce(() => ({
456+
choices: [{ message: { content: "response" } }],
457+
}))
458+
459+
await handler.completePrompt("test prompt")
460+
461+
const callArgs = mockCreate.mock.calls[0][0]
462+
expect(callArgs).toHaveProperty("max_tokens")
463+
expect(callArgs).not.toHaveProperty("max_completion_tokens")
464+
})
465+
})
373466
})

src/api/providers/qwen-code.ts

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -219,16 +219,19 @@ export class QwenCodeHandler extends BaseProvider implements SingleCompletionHan
219219

220220
const convertedMessages = [systemMessage, ...convertToOpenAiMessages(messages)]
221221

222+
// DashScope's OpenAI-compatible API does not support several OpenAI-specific
223+
// parameters. Using them causes a 400 Bad Request error. Specifically:
224+
// - max_completion_tokens -> use max_tokens instead
225+
// - parallel_tool_calls -> not supported
226+
// - stream_options -> not supported
222227
const requestOptions: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = {
223228
model: model.id,
224229
temperature: 0,
225230
messages: convertedMessages,
226231
stream: true,
227-
stream_options: { include_usage: true },
228-
max_completion_tokens: model.info.maxTokens,
232+
max_tokens: model.info.maxTokens,
229233
tools: this.convertToolsForOpenAI(metadata?.tools),
230234
tool_choice: metadata?.tool_choice,
231-
parallel_tool_calls: metadata?.parallelToolCalls ?? true,
232235
}
233236

234237
const stream = await this.callApiWithRetry(() => client.chat.completions.create(requestOptions))
@@ -321,6 +324,28 @@ export class QwenCodeHandler extends BaseProvider implements SingleCompletionHan
321324
return { id, info }
322325
}
323326

327+
/**
328+
* Override to skip strict mode for DashScope compatibility.
329+
* DashScope's OpenAI-compatible API does not support OpenAI's strict mode
330+
* on tool definitions (strict: true, additionalProperties: false enforcement).
331+
* Sending these causes a 400 Bad Request error.
332+
*/
333+
protected override convertToolsForOpenAI(tools: any[] | undefined): any[] | undefined {
334+
if (!tools) {
335+
return undefined
336+
}
337+
338+
return tools
339+
.filter((tool) => tool.type === "function")
340+
.map((tool) => ({
341+
...tool,
342+
function: {
343+
...tool.function,
344+
// Do not set strict: true - DashScope does not support it
345+
},
346+
}))
347+
}
348+
324349
async completePrompt(prompt: string): Promise<string> {
325350
await this.ensureAuthenticated()
326351
const client = this.ensureClient()
@@ -329,7 +354,7 @@ export class QwenCodeHandler extends BaseProvider implements SingleCompletionHan
329354
const requestOptions: OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming = {
330355
model: model.id,
331356
messages: [{ role: "user", content: prompt }],
332-
max_completion_tokens: model.info.maxTokens,
357+
max_tokens: model.info.maxTokens,
333358
}
334359

335360
const response = await this.callApiWithRetry(() => client.chat.completions.create(requestOptions))

0 commit comments

Comments
 (0)