mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
fix: improve OpenAI error handling to show detailed 400 error messages
- Extract detailed error information from APIError instances - Handle nested error structures from OpenAI API responses - Include status codes and error types in error messages - Add comprehensive test coverage for various error scenarios Fixes #9662
This commit is contained in:
parent
e682c039de
commit
72579343c5
2 changed files with 339 additions and 18 deletions
243
src/api/providers/utils/__tests__/openai-error-handler.spec.ts
Normal file
243
src/api/providers/utils/__tests__/openai-error-handler.spec.ts
Normal file
|
|
@ -0,0 +1,243 @@
|
|||
import { describe, it, expect, vi, beforeEach } from "vitest"
|
||||
import { APIError } from "openai"
|
||||
import { handleOpenAIError } from "../openai-error-handler"
|
||||
|
||||
// Mock i18n
|
||||
vi.mock("../../../../i18n/setup", () => ({
|
||||
default: {
|
||||
t: (key: string) => key,
|
||||
},
|
||||
}))
|
||||
|
||||
describe("handleOpenAIError", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.spyOn(console, "error").mockImplementation(() => {})
|
||||
})
|
||||
|
||||
describe("APIError handling", () => {
|
||||
it("should extract detailed error message from APIError with status 400", () => {
|
||||
const error = Object.create(APIError.prototype)
|
||||
Object.assign(error, {
|
||||
status: 400,
|
||||
message: "Bad Request",
|
||||
error: {
|
||||
message: "Invalid request: missing required parameter 'model'",
|
||||
type: "invalid_request_error",
|
||||
},
|
||||
})
|
||||
|
||||
const result = handleOpenAIError(error, "openai")
|
||||
expect(result.message).toBe(
|
||||
"openai completion error: 400 \"invalid_request_error\" Invalid request: missing required parameter 'model'",
|
||||
)
|
||||
})
|
||||
|
||||
it("should handle nested error structure", () => {
|
||||
const error = Object.create(APIError.prototype)
|
||||
Object.assign(error, {
|
||||
status: 400,
|
||||
error: {
|
||||
error: {
|
||||
message: "The model `gpt-4-turbo` does not exist",
|
||||
type: "invalid_request_error",
|
||||
code: "model_not_found",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const result = handleOpenAIError(error, "openai")
|
||||
expect(result.message).toBe(
|
||||
'openai completion error: 400 "invalid_request_error" The model `gpt-4-turbo` does not exist',
|
||||
)
|
||||
})
|
||||
|
||||
it("should include status code when available", () => {
|
||||
const error = Object.create(APIError.prototype)
|
||||
Object.assign(error, {
|
||||
status: 401,
|
||||
message: "Unauthorized",
|
||||
error: {
|
||||
message: "Invalid API key provided",
|
||||
type: "authentication_error",
|
||||
},
|
||||
})
|
||||
|
||||
const result = handleOpenAIError(error, "openai")
|
||||
expect(result.message).toBe('openai completion error: 401 "authentication_error" Invalid API key provided')
|
||||
})
|
||||
|
||||
it("should handle 429 rate limit errors", () => {
|
||||
const error = Object.create(APIError.prototype)
|
||||
Object.assign(error, {
|
||||
status: 429,
|
||||
message: "Too Many Requests",
|
||||
error: {
|
||||
message: "Rate limit exceeded. Please try again in 20 seconds.",
|
||||
type: "rate_limit_error",
|
||||
},
|
||||
})
|
||||
|
||||
const result = handleOpenAIError(error, "openai")
|
||||
expect(result.message).toBe(
|
||||
'openai completion error: 429 "rate_limit_error" Rate limit exceeded. Please try again in 20 seconds.',
|
||||
)
|
||||
})
|
||||
|
||||
it("should handle errors with code property", () => {
|
||||
const error = Object.create(APIError.prototype)
|
||||
Object.assign(error, {
|
||||
status: 400,
|
||||
code: "context_length_exceeded",
|
||||
message: "Context length exceeded",
|
||||
})
|
||||
|
||||
const result = handleOpenAIError(error, "openai")
|
||||
expect(result.message).toBe(
|
||||
'openai completion error: 400 "context_length_exceeded" Context length exceeded',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Regular Error handling", () => {
|
||||
it("should handle regular Error instances", () => {
|
||||
const error = new Error("Connection timeout")
|
||||
const result = handleOpenAIError(error, "openai")
|
||||
expect(result.message).toBe("openai completion error: Connection timeout")
|
||||
})
|
||||
|
||||
it("should handle axios-like errors with response data", () => {
|
||||
const error = new Error("Request failed")
|
||||
;(error as any).response = {
|
||||
data: {
|
||||
error: {
|
||||
message: "Invalid JSON in request body",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const result = handleOpenAIError(error, "openai")
|
||||
expect(result.message).toBe("openai completion error: Invalid JSON in request body")
|
||||
})
|
||||
|
||||
it("should handle errors with response.data.message", () => {
|
||||
const error = new Error("Request failed")
|
||||
;(error as any).response = {
|
||||
data: {
|
||||
message: "Request body too large",
|
||||
},
|
||||
}
|
||||
|
||||
const result = handleOpenAIError(error, "openai")
|
||||
expect(result.message).toBe("openai completion error: Request body too large")
|
||||
})
|
||||
})
|
||||
|
||||
describe("Plain object error handling", () => {
|
||||
it("should handle plain objects with error.message", () => {
|
||||
const error = {
|
||||
error: {
|
||||
message: "Invalid parameters",
|
||||
},
|
||||
}
|
||||
|
||||
const result = handleOpenAIError(error, "openai")
|
||||
expect(result.message).toBe("openai completion error: Invalid parameters")
|
||||
})
|
||||
|
||||
it("should handle plain objects with message property", () => {
|
||||
const error = {
|
||||
message: "Service unavailable",
|
||||
}
|
||||
|
||||
const result = handleOpenAIError(error, "openai")
|
||||
expect(result.message).toBe("openai completion error: Service unavailable")
|
||||
})
|
||||
|
||||
it("should handle plain objects with detail property", () => {
|
||||
const error = {
|
||||
detail: "Authentication failed: token expired",
|
||||
}
|
||||
|
||||
const result = handleOpenAIError(error, "openai")
|
||||
expect(result.message).toBe("openai completion error: Authentication failed: token expired")
|
||||
})
|
||||
})
|
||||
|
||||
describe("Special error cases", () => {
|
||||
it("should handle ByteString conversion errors", () => {
|
||||
const error = new Error(
|
||||
"Cannot convert argument to a ByteString because the character at index 5 has value 65533",
|
||||
)
|
||||
const result = handleOpenAIError(error, "openai")
|
||||
expect(result.message).toBe("common:errors.api.invalidKeyInvalidChars")
|
||||
})
|
||||
|
||||
it("should handle null errors", () => {
|
||||
const result = handleOpenAIError(null, "openai")
|
||||
expect(result.message).toBe("openai completion error: null")
|
||||
})
|
||||
|
||||
it("should handle undefined errors", () => {
|
||||
const result = handleOpenAIError(undefined, "openai")
|
||||
expect(result.message).toBe("openai completion error: undefined")
|
||||
})
|
||||
|
||||
it("should handle string errors", () => {
|
||||
const result = handleOpenAIError("Network error", "openai")
|
||||
expect(result.message).toBe("openai completion error: Network error")
|
||||
})
|
||||
|
||||
it("should handle number errors", () => {
|
||||
const result = handleOpenAIError(500, "openai")
|
||||
expect(result.message).toBe("openai completion error: 500")
|
||||
})
|
||||
})
|
||||
|
||||
describe("Provider name handling", () => {
|
||||
it("should use the correct provider name in error messages", () => {
|
||||
const error = new Error("API error")
|
||||
|
||||
const result1 = handleOpenAIError(error, "anthropic")
|
||||
expect(result1.message).toBe("anthropic completion error: API error")
|
||||
|
||||
const result2 = handleOpenAIError(error, "openai-compatible")
|
||||
expect(result2.message).toBe("openai-compatible completion error: API error")
|
||||
})
|
||||
})
|
||||
|
||||
describe("Logging", () => {
|
||||
it("should log error details for debugging", () => {
|
||||
const consoleSpy = vi.spyOn(console, "error")
|
||||
const error = new Error("Test error")
|
||||
|
||||
handleOpenAIError(error, "openai")
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
"[openai] API error:",
|
||||
expect.objectContaining({
|
||||
message: "Test error",
|
||||
error: expect.objectContaining({
|
||||
name: "Error",
|
||||
stack: expect.any(String),
|
||||
}),
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it("should log non-Error objects", () => {
|
||||
const consoleSpy = vi.spyOn(console, "error")
|
||||
const error = { custom: "error object" }
|
||||
|
||||
handleOpenAIError(error, "openai")
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
"[openai] API error:",
|
||||
expect.objectContaining({
|
||||
message: "[object Object]",
|
||||
error: { custom: "error object" },
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -3,8 +3,86 @@
|
|||
* Transforms technical errors into user-friendly messages
|
||||
*/
|
||||
|
||||
import { APIError } from "openai"
|
||||
import i18n from "../../../i18n/setup"
|
||||
|
||||
/**
|
||||
* Extracts detailed error information from various error structures
|
||||
* @param error - The error object to extract details from
|
||||
* @returns A detailed error message string
|
||||
*/
|
||||
function extractDetailedErrorMessage(error: unknown): string {
|
||||
// Handle OpenAI SDK APIError instances
|
||||
if (error instanceof APIError) {
|
||||
const parts: string[] = []
|
||||
|
||||
// Add status code if available
|
||||
if (error.status) {
|
||||
parts.push(`${error.status}`)
|
||||
}
|
||||
|
||||
// Extract the most detailed error message available
|
||||
let detailedMessage = ""
|
||||
|
||||
// Check for nested error structure (common in OpenAI responses)
|
||||
const errorObj = error as any
|
||||
if (errorObj.error?.error?.message) {
|
||||
detailedMessage = errorObj.error.error.message
|
||||
} else if (errorObj.error?.message) {
|
||||
detailedMessage = errorObj.error.message
|
||||
} else if (errorObj.message) {
|
||||
detailedMessage = errorObj.message
|
||||
}
|
||||
|
||||
// Add error type/code if available
|
||||
if (errorObj.error?.error?.type) {
|
||||
parts.push(`"${errorObj.error.error.type}"`)
|
||||
} else if (errorObj.error?.type) {
|
||||
parts.push(`"${errorObj.error.type}"`)
|
||||
} else if (errorObj.code) {
|
||||
parts.push(`"${errorObj.code}"`)
|
||||
}
|
||||
|
||||
// Add the detailed message
|
||||
if (detailedMessage) {
|
||||
parts.push(detailedMessage)
|
||||
}
|
||||
|
||||
return parts.join(" ")
|
||||
}
|
||||
|
||||
// Handle regular Error instances
|
||||
if (error instanceof Error) {
|
||||
// Try to extract more details from error properties
|
||||
const errorObj = error as any
|
||||
|
||||
// Check for response data in axios-like errors
|
||||
if (errorObj.response?.data?.error?.message) {
|
||||
return errorObj.response.data.error.message
|
||||
} else if (errorObj.response?.data?.message) {
|
||||
return errorObj.response.data.message
|
||||
}
|
||||
|
||||
return error.message || ""
|
||||
}
|
||||
|
||||
// Handle plain objects with error information
|
||||
if (typeof error === "object" && error !== null) {
|
||||
const errorObj = error as any
|
||||
|
||||
// Try various common error message locations
|
||||
if (errorObj.error?.message) {
|
||||
return errorObj.error.message
|
||||
} else if (errorObj.message) {
|
||||
return errorObj.message
|
||||
} else if (errorObj.detail) {
|
||||
return errorObj.detail
|
||||
}
|
||||
}
|
||||
|
||||
return String(error)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles OpenAI client errors and transforms them into user-friendly messages
|
||||
* @param error - The error to handle
|
||||
|
|
@ -12,26 +90,26 @@ import i18n from "../../../i18n/setup"
|
|||
* @returns The original error or a transformed user-friendly error
|
||||
*/
|
||||
export function handleOpenAIError(error: unknown, providerName: string): Error {
|
||||
if (error instanceof Error) {
|
||||
const msg = error.message || ""
|
||||
// Extract detailed error message
|
||||
const detailedMessage = extractDetailedErrorMessage(error)
|
||||
|
||||
// Log the original error details for debugging
|
||||
console.error(`[${providerName}] API error:`, {
|
||||
message: msg,
|
||||
name: error.name,
|
||||
stack: error.stack,
|
||||
})
|
||||
// Log the original error details for debugging
|
||||
console.error(`[${providerName}] API error:`, {
|
||||
message: detailedMessage,
|
||||
error:
|
||||
error instanceof Error
|
||||
? {
|
||||
name: error.name,
|
||||
stack: error.stack,
|
||||
}
|
||||
: error,
|
||||
})
|
||||
|
||||
// Invalid character/ByteString conversion error in API key
|
||||
if (msg.includes("Cannot convert argument to a ByteString")) {
|
||||
return new Error(i18n.t("common:errors.api.invalidKeyInvalidChars"))
|
||||
}
|
||||
|
||||
// For other Error instances, wrap with provider-specific prefix
|
||||
return new Error(`${providerName} completion error: ${msg}`)
|
||||
// Invalid character/ByteString conversion error in API key
|
||||
if (detailedMessage.includes("Cannot convert argument to a ByteString")) {
|
||||
return new Error(i18n.t("common:errors.api.invalidKeyInvalidChars"))
|
||||
}
|
||||
|
||||
// Non-Error: wrap with provider-specific prefix
|
||||
console.error(`[${providerName}] Non-Error exception:`, error)
|
||||
return new Error(`${providerName} completion error: ${String(error)}`)
|
||||
// Return error with detailed message
|
||||
return new Error(`${providerName} completion error: ${detailedMessage}`)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue