mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-10-06 02:47:56 +00:00
fix: improve error handling for Qwen API 400 status codes
- Add comprehensive error handling in QwenCodeHandler.callApiWithRetry - Handle 400, 403, 429, and 5xx errors with user-friendly messages - Add try-catch blocks to createMessage and completePrompt methods - Include detailed error context to help users understand issues - Add comprehensive test coverage for error scenarios Fixes #8142
This commit is contained in:
parent
87b45def18
commit
37925d71ce
2 changed files with 309 additions and 71 deletions
191
src/api/providers/__tests__/qwen-code.spec.ts
Normal file
191
src/api/providers/__tests__/qwen-code.spec.ts
Normal file
|
|
@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -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("<think>") || newText.includes("</think>")) {
|
||||
// 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("<think>") || newText.includes("</think>")) {
|
||||
// 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<string> {
|
||||
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 || ""
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue