mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
fix: improve error handling for OpenAI Compatible API providers
- Add detailed error messages with context-specific recovery suggestions - Handle various error types (401, 404, 429, connection, timeout, etc.) - Provide actionable guidance for users to resolve issues - Add comprehensive tests for error handling scenarios - Update existing provider tests to match new error format Fixes #7486
This commit is contained in:
parent
548d3b48a9
commit
f577ee252c
10 changed files with 319 additions and 36 deletions
|
|
@ -0,0 +1,225 @@
|
|||
import { describe, it, expect, vi, beforeEach } from "vitest"
|
||||
import { BaseOpenAiCompatibleProvider } from "../base-openai-compatible-provider"
|
||||
import type { ApiHandlerOptions } from "../../../shared/api"
|
||||
import type { ModelInfo } from "@roo-code/types"
|
||||
|
||||
// Create a concrete implementation for testing
|
||||
class TestOpenAiCompatibleProvider extends BaseOpenAiCompatibleProvider<"test-model"> {
|
||||
constructor(options: ApiHandlerOptions) {
|
||||
super({
|
||||
providerName: "TestProvider",
|
||||
baseURL: options.openAiBaseUrl || "https://api.test.com/v1",
|
||||
defaultProviderModelId: "test-model",
|
||||
providerModels: {
|
||||
"test-model": {
|
||||
contextWindow: 4096,
|
||||
maxTokens: 1000,
|
||||
supportsPromptCache: false,
|
||||
} as ModelInfo,
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
describe("BaseOpenAiCompatibleProvider Error Handling", () => {
|
||||
let provider: TestOpenAiCompatibleProvider
|
||||
const mockApiKey = "test-api-key"
|
||||
const mockBaseUrl = "https://api.test.com/v1"
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe("enhanceErrorMessage", () => {
|
||||
it("should enhance 401 unauthorized errors with helpful suggestions", async () => {
|
||||
provider = new TestOpenAiCompatibleProvider({
|
||||
apiKey: mockApiKey,
|
||||
openAiBaseUrl: mockBaseUrl,
|
||||
})
|
||||
|
||||
const mockError = new Error("Unauthorized")
|
||||
;(mockError as any).status = 401
|
||||
|
||||
// Access private method through type assertion
|
||||
const enhancedError = (provider as any).enhanceErrorMessage(mockError)
|
||||
|
||||
expect(enhancedError.message).toContain("OpenAI Compatible API Error (TestProvider)")
|
||||
expect(enhancedError.message).toContain("Verify your API key is correct")
|
||||
expect(enhancedError.message).toContain("Check if the API key format matches")
|
||||
})
|
||||
|
||||
it("should enhance 404 not found errors with model and URL suggestions", async () => {
|
||||
provider = new TestOpenAiCompatibleProvider({
|
||||
apiKey: mockApiKey,
|
||||
openAiBaseUrl: mockBaseUrl,
|
||||
apiModelId: "custom-model",
|
||||
})
|
||||
|
||||
const mockError = new Error("Not Found")
|
||||
;(mockError as any).status = 404
|
||||
|
||||
const enhancedError = (provider as any).enhanceErrorMessage(mockError)
|
||||
|
||||
expect(enhancedError.message).toContain("OpenAI Compatible API Error (TestProvider)")
|
||||
expect(enhancedError.message).toContain(`Verify the base URL is correct: ${mockBaseUrl}`)
|
||||
expect(enhancedError.message).toContain("Check if the model 'custom-model' is available")
|
||||
expect(enhancedError.message).toContain("Ensure the API endpoint path is correct")
|
||||
})
|
||||
|
||||
it("should enhance rate limit errors with appropriate suggestions", async () => {
|
||||
provider = new TestOpenAiCompatibleProvider({
|
||||
apiKey: mockApiKey,
|
||||
openAiBaseUrl: mockBaseUrl,
|
||||
})
|
||||
|
||||
const mockError = new Error("Rate limit exceeded")
|
||||
;(mockError as any).status = 429
|
||||
|
||||
const enhancedError = (provider as any).enhanceErrorMessage(mockError)
|
||||
|
||||
expect(enhancedError.message).toContain("You've hit the rate limit")
|
||||
expect(enhancedError.message).toContain("Wait a moment before retrying")
|
||||
expect(enhancedError.message).toContain("Consider upgrading your API plan")
|
||||
})
|
||||
|
||||
it("should enhance connection errors with network troubleshooting tips", async () => {
|
||||
provider = new TestOpenAiCompatibleProvider({
|
||||
apiKey: mockApiKey,
|
||||
openAiBaseUrl: mockBaseUrl,
|
||||
})
|
||||
|
||||
const mockError = new Error("ECONNREFUSED")
|
||||
|
||||
const enhancedError = (provider as any).enhanceErrorMessage(mockError)
|
||||
|
||||
expect(enhancedError.message).toContain(`Cannot connect to ${mockBaseUrl}`)
|
||||
expect(enhancedError.message).toContain("Verify the server is running")
|
||||
expect(enhancedError.message).toContain("Check your network connection")
|
||||
})
|
||||
|
||||
it("should enhance timeout errors with performance suggestions", async () => {
|
||||
provider = new TestOpenAiCompatibleProvider({
|
||||
apiKey: mockApiKey,
|
||||
openAiBaseUrl: mockBaseUrl,
|
||||
})
|
||||
|
||||
const mockError = new Error("Request timeout")
|
||||
|
||||
const enhancedError = (provider as any).enhanceErrorMessage(mockError)
|
||||
|
||||
expect(enhancedError.message).toContain("The request timed out")
|
||||
expect(enhancedError.message).toContain("server might be overloaded")
|
||||
expect(enhancedError.message).toContain("Try with a simpler request")
|
||||
})
|
||||
|
||||
it("should enhance model not found errors", async () => {
|
||||
provider = new TestOpenAiCompatibleProvider({
|
||||
apiKey: mockApiKey,
|
||||
openAiBaseUrl: mockBaseUrl,
|
||||
apiModelId: "nonexistent-model",
|
||||
})
|
||||
|
||||
const mockError = new Error("model 'nonexistent-model' not found")
|
||||
|
||||
const enhancedError = (provider as any).enhanceErrorMessage(mockError)
|
||||
|
||||
expect(enhancedError.message).toContain("The model 'nonexistent-model' may not be available")
|
||||
expect(enhancedError.message).toContain("Check the available models")
|
||||
expect(enhancedError.message).toContain("Try using a different model")
|
||||
})
|
||||
|
||||
it("should provide general suggestions for unknown errors", async () => {
|
||||
provider = new TestOpenAiCompatibleProvider({
|
||||
apiKey: mockApiKey,
|
||||
openAiBaseUrl: mockBaseUrl,
|
||||
})
|
||||
|
||||
const mockError = new Error("Some unknown error")
|
||||
|
||||
const enhancedError = (provider as any).enhanceErrorMessage(mockError)
|
||||
|
||||
expect(enhancedError.message).toContain("Verify your API configuration")
|
||||
expect(enhancedError.message).toContain("Check if the provider service is operational")
|
||||
expect(enhancedError.message).toContain("Try breaking down your request")
|
||||
expect(enhancedError.message).toContain("Consult your provider's documentation")
|
||||
})
|
||||
|
||||
it("should preserve original error properties", async () => {
|
||||
provider = new TestOpenAiCompatibleProvider({
|
||||
apiKey: mockApiKey,
|
||||
openAiBaseUrl: mockBaseUrl,
|
||||
})
|
||||
|
||||
const mockError = new Error("Server error")
|
||||
;(mockError as any).status = 500
|
||||
;(mockError as any).code = "INTERNAL_ERROR"
|
||||
|
||||
const enhancedError = (provider as any).enhanceErrorMessage(mockError)
|
||||
|
||||
expect((enhancedError as any).status).toBe(500)
|
||||
expect((enhancedError as any).code).toBe("INTERNAL_ERROR")
|
||||
})
|
||||
|
||||
it("should handle server errors (500, 502, 503) with appropriate suggestions", async () => {
|
||||
provider = new TestOpenAiCompatibleProvider({
|
||||
apiKey: mockApiKey,
|
||||
openAiBaseUrl: mockBaseUrl,
|
||||
})
|
||||
|
||||
const serverErrors = [500, 502, 503]
|
||||
|
||||
for (const status of serverErrors) {
|
||||
const mockError = new Error("Server error")
|
||||
;(mockError as any).status = status
|
||||
|
||||
const enhancedError = (provider as any).enhanceErrorMessage(mockError)
|
||||
|
||||
expect(enhancedError.message).toContain("The API server is experiencing issues")
|
||||
expect(enhancedError.message).toContain("Try again in a few moments")
|
||||
expect(enhancedError.message).toContain("Check your provider's status page")
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("createMessage error handling", () => {
|
||||
it("should throw enhanced error when stream creation fails", async () => {
|
||||
provider = new TestOpenAiCompatibleProvider({
|
||||
apiKey: mockApiKey,
|
||||
openAiBaseUrl: mockBaseUrl,
|
||||
})
|
||||
|
||||
// Mock the createStream method to throw an error
|
||||
const mockError = new Error("Connection failed")
|
||||
;(mockError as any).status = 500
|
||||
vi.spyOn(provider as any, "createStream").mockRejectedValue(mockError)
|
||||
|
||||
const generator = provider.createMessage("system prompt", [])
|
||||
|
||||
// Consume the generator and expect it to throw
|
||||
await expect(async () => {
|
||||
for await (const chunk of generator) {
|
||||
// This should not be reached
|
||||
}
|
||||
}).rejects.toThrow("OpenAI Compatible API Error (TestProvider)")
|
||||
})
|
||||
})
|
||||
|
||||
describe("completePrompt error handling", () => {
|
||||
it("should throw enhanced error when completion fails", async () => {
|
||||
provider = new TestOpenAiCompatibleProvider({
|
||||
apiKey: mockApiKey,
|
||||
openAiBaseUrl: mockBaseUrl,
|
||||
})
|
||||
|
||||
// Mock the client to throw an error
|
||||
const mockError = new Error("API key invalid")
|
||||
;(mockError as any).status = 401
|
||||
vi.spyOn((provider as any).client.chat.completions, "create").mockRejectedValue(mockError)
|
||||
|
||||
await expect(provider.completePrompt("test prompt")).rejects.toThrow(
|
||||
"OpenAI Compatible API Error (TestProvider)",
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -307,7 +307,7 @@ describe("ChutesHandler", () => {
|
|||
it("should handle errors in completePrompt", async () => {
|
||||
const errorMessage = "Chutes API error"
|
||||
mockCreate.mockRejectedValueOnce(new Error(errorMessage))
|
||||
await expect(handler.completePrompt("test prompt")).rejects.toThrow(`Chutes completion error: ${errorMessage}`)
|
||||
await expect(handler.completePrompt("test prompt")).rejects.toThrow("OpenAI Compatible API Error (Chutes)")
|
||||
})
|
||||
|
||||
it("createMessage should yield text content from stream", async () => {
|
||||
|
|
|
|||
|
|
@ -178,9 +178,7 @@ describe("FeatherlessHandler", () => {
|
|||
it("should handle errors in completePrompt", async () => {
|
||||
const errorMessage = "Featherless API error"
|
||||
mockCreate.mockRejectedValueOnce(new Error(errorMessage))
|
||||
await expect(handler.completePrompt("test prompt")).rejects.toThrow(
|
||||
`Featherless completion error: ${errorMessage}`,
|
||||
)
|
||||
await expect(handler.completePrompt("test prompt")).rejects.toThrow("OpenAI Compatible API Error (Featherless)")
|
||||
})
|
||||
|
||||
it("createMessage should yield text content from stream", async () => {
|
||||
|
|
|
|||
|
|
@ -294,9 +294,7 @@ describe("FireworksHandler", () => {
|
|||
it("should handle errors in completePrompt", async () => {
|
||||
const errorMessage = "Fireworks API error"
|
||||
mockCreate.mockRejectedValueOnce(new Error(errorMessage))
|
||||
await expect(handler.completePrompt("test prompt")).rejects.toThrow(
|
||||
`Fireworks completion error: ${errorMessage}`,
|
||||
)
|
||||
await expect(handler.completePrompt("test prompt")).rejects.toThrow("OpenAI Compatible API Error (Fireworks)")
|
||||
})
|
||||
|
||||
it("createMessage should yield text content from stream", async () => {
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ describe("GroqHandler", () => {
|
|||
it("should handle errors in completePrompt", async () => {
|
||||
const errorMessage = "Groq API error"
|
||||
mockCreate.mockRejectedValueOnce(new Error(errorMessage))
|
||||
await expect(handler.completePrompt("test prompt")).rejects.toThrow(`Groq completion error: ${errorMessage}`)
|
||||
await expect(handler.completePrompt("test prompt")).rejects.toThrow("OpenAI Compatible API Error (Groq)")
|
||||
})
|
||||
|
||||
it("createMessage should yield text content from stream", async () => {
|
||||
|
|
|
|||
|
|
@ -196,7 +196,7 @@ describe("IOIntelligenceHandler", () => {
|
|||
const errorMessage = "IO Intelligence API error"
|
||||
mockCreate.mockRejectedValueOnce(new Error(errorMessage))
|
||||
await expect(handler.completePrompt("test prompt")).rejects.toThrow(
|
||||
`IO Intelligence completion error: ${errorMessage}`,
|
||||
"OpenAI Compatible API Error (IO Intelligence)",
|
||||
)
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -283,7 +283,7 @@ describe("RooHandler", () => {
|
|||
it("should handle API errors", async () => {
|
||||
mockCreate.mockRejectedValueOnce(new Error("API Error"))
|
||||
await expect(handler.completePrompt("Test prompt")).rejects.toThrow(
|
||||
"Roo Code Cloud completion error: API Error",
|
||||
"OpenAI Compatible API Error (Roo Code Cloud)",
|
||||
)
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -65,9 +65,7 @@ describe("SambaNovaHandler", () => {
|
|||
it("should handle errors in completePrompt", async () => {
|
||||
const errorMessage = "SambaNova API error"
|
||||
mockCreate.mockRejectedValueOnce(new Error(errorMessage))
|
||||
await expect(handler.completePrompt("test prompt")).rejects.toThrow(
|
||||
`SambaNova completion error: ${errorMessage}`,
|
||||
)
|
||||
await expect(handler.completePrompt("test prompt")).rejects.toThrow("OpenAI Compatible API Error (SambaNova)")
|
||||
})
|
||||
|
||||
it("createMessage should yield text content from stream", async () => {
|
||||
|
|
|
|||
|
|
@ -137,9 +137,7 @@ describe("ZAiHandler", () => {
|
|||
it("should handle errors in completePrompt", async () => {
|
||||
const errorMessage = "Z AI API error"
|
||||
mockCreate.mockRejectedValueOnce(new Error(errorMessage))
|
||||
await expect(handler.completePrompt("test prompt")).rejects.toThrow(
|
||||
`Z AI completion error: ${errorMessage}`,
|
||||
)
|
||||
await expect(handler.completePrompt("test prompt")).rejects.toThrow("OpenAI Compatible API Error (Z AI)")
|
||||
})
|
||||
|
||||
it("createMessage should yield text content from stream", async () => {
|
||||
|
|
|
|||
|
|
@ -94,28 +94,97 @@ export abstract class BaseOpenAiCompatibleProvider<ModelName extends string>
|
|||
messages: Anthropic.Messages.MessageParam[],
|
||||
metadata?: ApiHandlerCreateMessageMetadata,
|
||||
): ApiStream {
|
||||
const stream = await this.createStream(systemPrompt, messages, metadata)
|
||||
try {
|
||||
const stream = await this.createStream(systemPrompt, messages, metadata)
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
|
||||
if (delta?.content) {
|
||||
yield {
|
||||
type: "text",
|
||||
text: delta.content,
|
||||
}
|
||||
}
|
||||
|
||||
if (chunk.usage) {
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: chunk.usage.prompt_tokens || 0,
|
||||
outputTokens: chunk.usage.completion_tokens || 0,
|
||||
if (delta?.content) {
|
||||
yield {
|
||||
type: "text",
|
||||
text: delta.content,
|
||||
}
|
||||
}
|
||||
|
||||
if (chunk.usage) {
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: chunk.usage.prompt_tokens || 0,
|
||||
outputTokens: chunk.usage.completion_tokens || 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Enhance error messages for OpenAI Compatible providers
|
||||
const enhancedError = this.enhanceErrorMessage(error)
|
||||
throw enhancedError
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhances error messages with helpful guidance for OpenAI Compatible API issues
|
||||
*/
|
||||
private enhanceErrorMessage(error: any): Error {
|
||||
const baseUrl = this.baseURL
|
||||
const modelId = this.options.apiModelId || this.defaultProviderModelId
|
||||
|
||||
let errorMessage = error?.message || "Unknown error occurred"
|
||||
let suggestions: string[] = []
|
||||
|
||||
// Check for common error patterns
|
||||
if (error?.status === 401 || errorMessage.includes("401") || errorMessage.includes("Unauthorized")) {
|
||||
suggestions.push("• Verify your API key is correct and has proper permissions")
|
||||
suggestions.push("• Check if the API key format matches your provider's requirements")
|
||||
} else if (error?.status === 404 || errorMessage.includes("404") || errorMessage.includes("Not Found")) {
|
||||
suggestions.push(`• Verify the base URL is correct: ${baseUrl}`)
|
||||
suggestions.push(`• Check if the model '${modelId}' is available on your provider`)
|
||||
suggestions.push("• Ensure the API endpoint path is correct (some providers use /v1, others don't)")
|
||||
} else if (error?.status === 429 || errorMessage.includes("429") || errorMessage.includes("rate limit")) {
|
||||
suggestions.push("• You've hit the rate limit for this API")
|
||||
suggestions.push("• Wait a moment before retrying")
|
||||
suggestions.push("• Consider upgrading your API plan for higher limits")
|
||||
} else if (error?.status === 500 || error?.status === 502 || error?.status === 503) {
|
||||
suggestions.push("• The API server is experiencing issues")
|
||||
suggestions.push("• Try again in a few moments")
|
||||
suggestions.push("• Check your provider's status page for outages")
|
||||
} else if (errorMessage.includes("ECONNREFUSED") || errorMessage.includes("ENOTFOUND")) {
|
||||
suggestions.push(`• Cannot connect to ${baseUrl}`)
|
||||
suggestions.push("• Verify the server is running and accessible")
|
||||
suggestions.push("• Check your network connection and firewall settings")
|
||||
} else if (errorMessage.includes("timeout") || errorMessage.includes("ETIMEDOUT")) {
|
||||
suggestions.push("• The request timed out")
|
||||
suggestions.push("• The server might be overloaded or the model is taking too long to respond")
|
||||
suggestions.push("• Try with a simpler request or a different model")
|
||||
} else if (errorMessage.includes("model") && errorMessage.includes("not")) {
|
||||
suggestions.push(`• The model '${modelId}' may not be available`)
|
||||
suggestions.push("• Check the available models for your provider")
|
||||
suggestions.push("• Try using a different model")
|
||||
}
|
||||
|
||||
// Add general suggestions if no specific ones were added
|
||||
if (suggestions.length === 0) {
|
||||
suggestions.push("• Verify your API configuration (base URL, API key, model)")
|
||||
suggestions.push("• Check if the provider service is operational")
|
||||
suggestions.push("• Try breaking down your request into smaller parts")
|
||||
suggestions.push("• Consult your provider's documentation for specific requirements")
|
||||
}
|
||||
|
||||
// Create enhanced error message
|
||||
const enhancedMessage = `OpenAI Compatible API Error (${this.providerName}):\n${errorMessage}\n\nSuggestions to resolve:\n${suggestions.join("\n")}`
|
||||
|
||||
const enhancedError = new Error(enhancedMessage)
|
||||
// Preserve original error properties
|
||||
if (error?.status) {
|
||||
;(enhancedError as any).status = error.status
|
||||
}
|
||||
if (error?.code) {
|
||||
;(enhancedError as any).code = error.code
|
||||
}
|
||||
|
||||
return enhancedError
|
||||
}
|
||||
|
||||
async completePrompt(prompt: string): Promise<string> {
|
||||
const { id: modelId } = this.getModel()
|
||||
|
||||
|
|
@ -127,11 +196,8 @@ export abstract class BaseOpenAiCompatibleProvider<ModelName extends string>
|
|||
|
||||
return response.choices[0]?.message.content || ""
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
throw new Error(`${this.providerName} completion error: ${error.message}`)
|
||||
}
|
||||
|
||||
throw error
|
||||
// Use the same enhanced error handling
|
||||
throw this.enhanceErrorMessage(error)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue