diff --git a/src/api/providers/__tests__/qwen-code.spec.ts b/src/api/providers/__tests__/qwen-code.spec.ts
new file mode 100644
index 0000000000..6cc05a91c6
--- /dev/null
+++ b/src/api/providers/__tests__/qwen-code.spec.ts
@@ -0,0 +1,191 @@
+import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"
+import { QwenCodeHandler } from "../qwen-code"
+import { promises as fs } from "node:fs"
+import OpenAI from "openai"
+
+// Mock fs module
+vi.mock("node:fs", () => ({
+ promises: {
+ readFile: vi.fn(),
+ writeFile: vi.fn(),
+ },
+}))
+
+// Mock OpenAI
+vi.mock("openai", () => {
+ const mockCreate = vi.fn()
+ return {
+ default: vi.fn().mockImplementation(() => ({
+ chat: {
+ completions: {
+ create: mockCreate,
+ },
+ },
+ })),
+ }
+})
+
+// Mock fetch for OAuth token refresh
+global.fetch = vi.fn()
+
+describe("QwenCodeHandler", () => {
+ let handler: QwenCodeHandler
+ const mockCredentials = {
+ access_token: "test-access-token",
+ refresh_token: "test-refresh-token",
+ token_type: "Bearer",
+ expiry_date: Date.now() + 3600000, // 1 hour from now
+ resource_url: "https://dashscope.aliyuncs.com/compatible-mode/v1",
+ }
+
+ beforeEach(() => {
+ vi.clearAllMocks()
+ // Mock reading credentials file
+ vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(mockCredentials))
+ vi.mocked(fs.writeFile).mockResolvedValue(undefined)
+
+ handler = new QwenCodeHandler({
+ apiModelId: "qwen-max",
+ })
+ })
+
+ afterEach(() => {
+ vi.clearAllMocks()
+ })
+
+ describe("Error Handling", () => {
+ it("should handle 400 errors with user-friendly message", async () => {
+ const mockClient = new OpenAI({ apiKey: "test" })
+ const mockError = {
+ status: 400,
+ message: "Invalid request format",
+ }
+
+ vi.mocked(mockClient.chat.completions.create).mockRejectedValue(mockError)
+
+ // Override the ensureClient method to return our mock
+ handler["client"] = mockClient
+
+ await expect(handler.completePrompt("test prompt")).rejects.toThrow(
+ "Qwen API Error (400): Invalid request format. This may be due to invalid input format, unsupported file type, or request size limits.",
+ )
+ })
+
+ it("should handle 401 errors and attempt token refresh", async () => {
+ const mockClient = new OpenAI({ apiKey: "test" })
+ const mockError = {
+ status: 401,
+ message: "Unauthorized",
+ }
+
+ // First call fails with 401, second succeeds after refresh
+ vi.mocked(mockClient.chat.completions.create)
+ .mockRejectedValueOnce(mockError)
+ .mockResolvedValueOnce({
+ choices: [{ message: { content: "Success after refresh" } }],
+ } as any)
+
+ // Mock successful token refresh
+ vi.mocked(global.fetch).mockResolvedValueOnce({
+ ok: true,
+ json: async () => ({
+ access_token: "new-access-token",
+ token_type: "Bearer",
+ expires_in: 3600,
+ refresh_token: "new-refresh-token",
+ }),
+ } as any)
+
+ handler["client"] = mockClient
+
+ const result = await handler.completePrompt("test prompt")
+ expect(result).toBe("Success after refresh")
+ expect(global.fetch).toHaveBeenCalledTimes(1)
+ })
+
+ it("should handle 403 errors with permission message", async () => {
+ const mockClient = new OpenAI({ apiKey: "test" })
+ const mockError = {
+ status: 403,
+ message: "Access denied",
+ }
+
+ vi.mocked(mockClient.chat.completions.create).mockRejectedValue(mockError)
+ handler["client"] = mockClient
+
+ await expect(handler.completePrompt("test prompt")).rejects.toThrow(
+ "Qwen API Error (403): Access denied. Please check your API permissions.",
+ )
+ })
+
+ it("should handle 429 rate limit errors", async () => {
+ const mockClient = new OpenAI({ apiKey: "test" })
+ const mockError = {
+ status: 429,
+ message: "Too many requests",
+ }
+
+ vi.mocked(mockClient.chat.completions.create).mockRejectedValue(mockError)
+ handler["client"] = mockClient
+
+ await expect(handler.completePrompt("test prompt")).rejects.toThrow(
+ "Qwen API Error (429): Too many requests. Please wait before making more requests.",
+ )
+ })
+
+ it("should handle 500+ server errors", async () => {
+ const mockClient = new OpenAI({ apiKey: "test" })
+ const mockError = {
+ status: 503,
+ message: "Service unavailable",
+ }
+
+ vi.mocked(mockClient.chat.completions.create).mockRejectedValue(mockError)
+ handler["client"] = mockClient
+
+ await expect(handler.completePrompt("test prompt")).rejects.toThrow(
+ "Qwen API Error (503): Service unavailable. The Qwen service may be temporarily unavailable.",
+ )
+ })
+
+ it("should handle generic errors with context", async () => {
+ const mockClient = new OpenAI({ apiKey: "test" })
+ const mockError = new Error("Network timeout")
+
+ vi.mocked(mockClient.chat.completions.create).mockRejectedValue(mockError)
+ handler["client"] = mockClient
+
+ await expect(handler.completePrompt("test prompt")).rejects.toThrow(
+ "Failed to complete prompt with Qwen model: Network timeout",
+ )
+ })
+
+ it("should preserve already formatted Qwen API errors", async () => {
+ const mockClient = new OpenAI({ apiKey: "test" })
+ const mockError = new Error("Qwen API Error (400): Already formatted error")
+
+ vi.mocked(mockClient.chat.completions.create).mockRejectedValue(mockError)
+ handler["client"] = mockClient
+
+ await expect(handler.completePrompt("test prompt")).rejects.toThrow(
+ "Qwen API Error (400): Already formatted error",
+ )
+ })
+ })
+
+ describe("Model Configuration", () => {
+ it("should return correct model info", () => {
+ const model = handler.getModel()
+ expect(model.id).toBe("qwen-max")
+ expect(model.info).toBeDefined()
+ expect(model.info.maxTokens).toBeGreaterThan(0)
+ })
+
+ it("should use default model when not specified", () => {
+ const defaultHandler = new QwenCodeHandler({})
+ const model = defaultHandler.getModel()
+ expect(model.id).toBeDefined()
+ expect(model.info).toBeDefined()
+ })
+ })
+})
diff --git a/src/api/providers/qwen-code.ts b/src/api/providers/qwen-code.ts
index d930d9dfc7..d7bade7716 100644
--- a/src/api/providers/qwen-code.ts
+++ b/src/api/providers/qwen-code.ts
@@ -195,92 +195,128 @@ export class QwenCodeHandler extends BaseProvider implements SingleCompletionHan
client.apiKey = this.credentials.access_token
client.baseURL = this.getBaseUrl(this.credentials)
return await apiCall()
+ } else if (error.status === 400) {
+ // Bad request error - provide more context
+ const errorMessage = error?.message || error?.error?.message || "Bad request"
+ const detailedMessage = `Qwen API Error (400): ${errorMessage}. This may be due to invalid input format, unsupported file type, or request size limits.`
+ console.error(detailedMessage)
+ throw new Error(detailedMessage)
+ } else if (error.status === 403) {
+ // Forbidden error
+ const errorMessage = error?.message || error?.error?.message || "Forbidden"
+ throw new Error(`Qwen API Error (403): ${errorMessage}. Please check your API permissions.`)
+ } else if (error.status === 429) {
+ // Rate limit error
+ const errorMessage = error?.message || error?.error?.message || "Rate limit exceeded"
+ throw new Error(`Qwen API Error (429): ${errorMessage}. Please wait before making more requests.`)
+ } else if (error.status >= 500) {
+ // Server error
+ const errorMessage = error?.message || error?.error?.message || "Server error"
+ throw new Error(
+ `Qwen API Error (${error.status}): ${errorMessage}. The Qwen service may be temporarily unavailable.`,
+ )
} else {
+ // Other errors - preserve original error but add context
+ if (error.status) {
+ const errorMessage = error?.message || error?.error?.message || "Unknown error"
+ throw new Error(`Qwen API Error (${error.status}): ${errorMessage}`)
+ }
throw error
}
}
}
override async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
- await this.ensureAuthenticated()
- const client = this.ensureClient()
- const model = this.getModel()
+ try {
+ await this.ensureAuthenticated()
+ const client = this.ensureClient()
+ const model = this.getModel()
- const systemMessage: OpenAI.Chat.ChatCompletionSystemMessageParam = {
- role: "system",
- content: systemPrompt,
- }
+ const systemMessage: OpenAI.Chat.ChatCompletionSystemMessageParam = {
+ role: "system",
+ content: systemPrompt,
+ }
- const convertedMessages = [systemMessage, ...convertToOpenAiMessages(messages)]
+ const convertedMessages = [systemMessage, ...convertToOpenAiMessages(messages)]
- 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,
- }
+ 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,
+ }
- const stream = await this.callApiWithRetry(() => client.chat.completions.create(requestOptions))
+ const stream = await this.callApiWithRetry(() => client.chat.completions.create(requestOptions))
- let fullContent = ""
+ let fullContent = ""
- for await (const apiChunk of stream) {
- const delta = apiChunk.choices[0]?.delta ?? {}
+ for await (const apiChunk of stream) {
+ const delta = apiChunk.choices[0]?.delta ?? {}
- if (delta.content) {
- let newText = delta.content
- if (newText.startsWith(fullContent)) {
- newText = newText.substring(fullContent.length)
- }
- fullContent = delta.content
+ if (delta.content) {
+ let newText = delta.content
+ if (newText.startsWith(fullContent)) {
+ newText = newText.substring(fullContent.length)
+ }
+ fullContent = delta.content
- if (newText) {
- // Check for thinking blocks
- if (newText.includes("") || newText.includes("")) {
- // Simple parsing for thinking blocks
- const parts = newText.split(/<\/?think>/g)
- for (let i = 0; i < parts.length; i++) {
- if (parts[i]) {
- if (i % 2 === 0) {
- // Outside thinking block
- yield {
- type: "text",
- text: parts[i],
- }
- } else {
- // Inside thinking block
- yield {
- type: "reasoning",
- text: parts[i],
+ if (newText) {
+ // Check for thinking blocks
+ if (newText.includes("") || newText.includes("")) {
+ // Simple parsing for thinking blocks
+ const parts = newText.split(/<\/?think>/g)
+ for (let i = 0; i < parts.length; i++) {
+ if (parts[i]) {
+ if (i % 2 === 0) {
+ // Outside thinking block
+ yield {
+ type: "text",
+ text: parts[i],
+ }
+ } else {
+ // Inside thinking block
+ yield {
+ type: "reasoning",
+ text: parts[i],
+ }
}
}
}
- }
- } else {
- yield {
- type: "text",
- text: newText,
+ } else {
+ yield {
+ type: "text",
+ text: newText,
+ }
}
}
}
- }
- if ("reasoning_content" in delta && delta.reasoning_content) {
- yield {
- type: "reasoning",
- text: (delta.reasoning_content as string | undefined) || "",
+ if ("reasoning_content" in delta && delta.reasoning_content) {
+ yield {
+ type: "reasoning",
+ text: (delta.reasoning_content as string | undefined) || "",
+ }
+ }
+
+ if (apiChunk.usage) {
+ yield {
+ type: "usage",
+ inputTokens: apiChunk.usage.prompt_tokens || 0,
+ outputTokens: apiChunk.usage.completion_tokens || 0,
+ }
}
}
+ } catch (error: any) {
+ // Log the error for debugging
+ console.error("Error in QwenCodeHandler.createMessage:", error)
- if (apiChunk.usage) {
- yield {
- type: "usage",
- inputTokens: apiChunk.usage.prompt_tokens || 0,
- outputTokens: apiChunk.usage.completion_tokens || 0,
- }
+ // Re-throw with a more user-friendly message if it's not already formatted
+ if (error.message && !error.message.startsWith("Qwen API Error")) {
+ throw new Error(`Failed to process request with Qwen model: ${error.message}`)
}
+ throw error
}
}
@@ -291,18 +327,29 @@ export class QwenCodeHandler extends BaseProvider implements SingleCompletionHan
}
async completePrompt(prompt: string): Promise {
- await this.ensureAuthenticated()
- const client = this.ensureClient()
- const model = this.getModel()
+ try {
+ await this.ensureAuthenticated()
+ const client = this.ensureClient()
+ const model = this.getModel()
- const requestOptions: OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming = {
- model: model.id,
- messages: [{ role: "user", content: prompt }],
- max_completion_tokens: model.info.maxTokens,
+ const requestOptions: OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming = {
+ model: model.id,
+ messages: [{ role: "user", content: prompt }],
+ max_completion_tokens: model.info.maxTokens,
+ }
+
+ const response = await this.callApiWithRetry(() => client.chat.completions.create(requestOptions))
+
+ return response.choices[0]?.message.content || ""
+ } catch (error: any) {
+ // Log the error for debugging
+ console.error("Error in QwenCodeHandler.completePrompt:", error)
+
+ // Re-throw with a more user-friendly message if it's not already formatted
+ if (error.message && !error.message.startsWith("Qwen API Error")) {
+ throw new Error(`Failed to complete prompt with Qwen model: ${error.message}`)
+ }
+ throw error
}
-
- const response = await this.callApiWithRetry(() => client.chat.completions.create(requestOptions))
-
- return response.choices[0]?.message.content || ""
}
}