Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 111 additions & 18 deletions src/api/providers/__tests__/qwen-code-native-tools.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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 {
Expand All @@ -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 () => {
Expand Down Expand Up @@ -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")
})
})
})
33 changes: 29 additions & 4 deletions src/api/providers/qwen-code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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()
Expand All @@ -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))
Expand Down
Loading