mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
fix: improve ChutesAI error handling for HTTP 500 errors
- Add retry logic with exponential backoff for transient 500 errors - Enhance error messages to provide more context when API returns empty response - Add detailed logging for debugging API errors - Preserve HTTP status codes in error objects for better error handling - Add comprehensive test coverage for error scenarios Fixes #7832
This commit is contained in:
parent
48d592f433
commit
365192829a
3 changed files with 239 additions and 47 deletions
|
|
@ -329,7 +329,64 @@ 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(/ChutesAI completion error/)
|
||||
})
|
||||
|
||||
it("should retry on 500 errors and succeed", async () => {
|
||||
const error500 = new Error("Internal Server Error")
|
||||
;(error500 as any).status = 500
|
||||
|
||||
// First attempt fails with 500, second succeeds
|
||||
mockCreate
|
||||
.mockRejectedValueOnce(error500)
|
||||
.mockResolvedValueOnce({ choices: [{ message: { content: "Success after retry" } }] })
|
||||
|
||||
const result = await handler.completePrompt("test prompt")
|
||||
expect(result).toBe("Success after retry")
|
||||
expect(mockCreate).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it("should handle 500 errors with empty response body", async () => {
|
||||
const error500 = new Error("")
|
||||
;(error500 as any).status = 500
|
||||
|
||||
// All attempts fail with empty error
|
||||
mockCreate.mockRejectedValue(error500)
|
||||
|
||||
await expect(handler.completePrompt("test prompt")).rejects.toThrow(/ChutesAI completion error.*500/)
|
||||
})
|
||||
|
||||
it("should not retry on 4xx errors", async () => {
|
||||
const error400 = new Error("Bad Request")
|
||||
;(error400 as any).status = 400
|
||||
|
||||
mockCreate.mockRejectedValueOnce(error400)
|
||||
|
||||
await expect(handler.completePrompt("test prompt")).rejects.toThrow(/ChutesAI completion error.*400/)
|
||||
expect(mockCreate).toHaveBeenCalledTimes(1) // Should not retry
|
||||
})
|
||||
|
||||
it("should handle streaming errors with retry", async () => {
|
||||
const error500 = new Error("Stream failed")
|
||||
;(error500 as any).status = 500
|
||||
|
||||
// First attempt fails, second succeeds
|
||||
mockCreate.mockRejectedValueOnce(error500).mockImplementationOnce(async () => ({
|
||||
[Symbol.asyncIterator]: async function* () {
|
||||
yield {
|
||||
choices: [{ delta: { content: "Retry success" } }],
|
||||
usage: null,
|
||||
}
|
||||
},
|
||||
}))
|
||||
|
||||
const stream = handler.createMessage("system", [])
|
||||
const chunks = []
|
||||
for await (const chunk of stream) {
|
||||
chunks.push(chunk)
|
||||
}
|
||||
|
||||
expect(chunks).toContainEqual({ type: "text", text: "Retry success" })
|
||||
})
|
||||
|
||||
it("createMessage should yield text content from stream", async () => {
|
||||
|
|
|
|||
|
|
@ -87,7 +87,24 @@ export abstract class BaseOpenAiCompatibleProvider<ModelName extends string>
|
|||
|
||||
try {
|
||||
return this.client.chat.completions.create(params, requestOptions)
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
// Log the raw error for debugging
|
||||
console.error(`${this.providerName} raw error:`, {
|
||||
message: error.message,
|
||||
status: error.status,
|
||||
statusText: error.statusText,
|
||||
response: error.response,
|
||||
cause: error.cause,
|
||||
stack: error.stack,
|
||||
})
|
||||
|
||||
// If it's an OpenAI API error with status code, preserve it
|
||||
if (error.status) {
|
||||
const enhancedError = handleOpenAIError(error, this.providerName)
|
||||
;(enhancedError as any).status = error.status
|
||||
throw enhancedError
|
||||
}
|
||||
|
||||
throw handleOpenAIError(error, this.providerName)
|
||||
}
|
||||
}
|
||||
|
|
@ -97,25 +114,44 @@ 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 (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: any) {
|
||||
// Log detailed error information
|
||||
console.error(`${this.providerName} streaming error:`, {
|
||||
message: error.message,
|
||||
status: error.status,
|
||||
statusText: error.statusText,
|
||||
type: error.type,
|
||||
code: error.code,
|
||||
})
|
||||
|
||||
if (chunk.usage) {
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: chunk.usage.prompt_tokens || 0,
|
||||
outputTokens: chunk.usage.completion_tokens || 0,
|
||||
}
|
||||
// Re-throw with status preserved
|
||||
if (error.status) {
|
||||
const enhancedError = new Error(error.message || `${this.providerName} streaming failed`)
|
||||
;(enhancedError as any).status = error.status
|
||||
throw enhancedError
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -129,7 +165,22 @@ export abstract class BaseOpenAiCompatibleProvider<ModelName extends string>
|
|||
})
|
||||
|
||||
return response.choices[0]?.message.content || ""
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
// Log the raw error for debugging
|
||||
console.error(`${this.providerName} completePrompt raw error:`, {
|
||||
message: error.message,
|
||||
status: error.status,
|
||||
statusText: error.statusText,
|
||||
response: error.response,
|
||||
})
|
||||
|
||||
// If it's an OpenAI API error with status code, preserve it
|
||||
if (error.status) {
|
||||
const enhancedError = handleOpenAIError(error, this.providerName)
|
||||
;(enhancedError as any).status = error.status
|
||||
throw enhancedError
|
||||
}
|
||||
|
||||
throw handleOpenAIError(error, this.providerName)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,9 @@ import { ApiStream } from "../transform/stream"
|
|||
import { BaseOpenAiCompatibleProvider } from "./base-openai-compatible-provider"
|
||||
|
||||
export class ChutesHandler extends BaseOpenAiCompatibleProvider<ChutesModelId> {
|
||||
private retryCount = 3
|
||||
private retryDelay = 1000 // Start with 1 second delay
|
||||
|
||||
constructor(options: ApiHandlerOptions) {
|
||||
super({
|
||||
...options,
|
||||
|
|
@ -47,46 +50,127 @@ export class ChutesHandler extends BaseOpenAiCompatibleProvider<ChutesModelId> {
|
|||
override async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
const model = this.getModel()
|
||||
|
||||
if (model.id.includes("DeepSeek-R1")) {
|
||||
const stream = await this.client.chat.completions.create({
|
||||
...this.getCompletionParams(systemPrompt, messages),
|
||||
messages: convertToR1Format([{ role: "user", content: systemPrompt }, ...messages]),
|
||||
})
|
||||
// Add retry logic for transient errors
|
||||
let lastError: Error | null = null
|
||||
for (let attempt = 0; attempt < this.retryCount; attempt++) {
|
||||
try {
|
||||
if (model.id.includes("DeepSeek-R1")) {
|
||||
const stream = await this.client.chat.completions.create({
|
||||
...this.getCompletionParams(systemPrompt, messages),
|
||||
messages: convertToR1Format([{ role: "user", content: systemPrompt }, ...messages]),
|
||||
})
|
||||
|
||||
const matcher = new XmlMatcher(
|
||||
"think",
|
||||
(chunk) =>
|
||||
({
|
||||
type: chunk.matched ? "reasoning" : "text",
|
||||
text: chunk.data,
|
||||
}) as const,
|
||||
)
|
||||
const matcher = new XmlMatcher(
|
||||
"think",
|
||||
(chunk) =>
|
||||
({
|
||||
type: chunk.matched ? "reasoning" : "text",
|
||||
text: chunk.data,
|
||||
}) as const,
|
||||
)
|
||||
|
||||
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) {
|
||||
for (const processedChunk of matcher.update(delta.content)) {
|
||||
if (delta?.content) {
|
||||
for (const processedChunk of matcher.update(delta.content)) {
|
||||
yield processedChunk
|
||||
}
|
||||
}
|
||||
|
||||
if (chunk.usage) {
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: chunk.usage.prompt_tokens || 0,
|
||||
outputTokens: chunk.usage.completion_tokens || 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process any remaining content
|
||||
for (const processedChunk of matcher.final()) {
|
||||
yield processedChunk
|
||||
}
|
||||
return // Success, exit the retry loop
|
||||
} else {
|
||||
yield* super.createMessage(systemPrompt, messages)
|
||||
return // Success, exit the retry loop
|
||||
}
|
||||
} catch (error: any) {
|
||||
lastError = error
|
||||
console.error(`ChutesAI API error (attempt ${attempt + 1}/${this.retryCount}):`, {
|
||||
status: error.status,
|
||||
message: error.message,
|
||||
response: error.response,
|
||||
cause: error.cause,
|
||||
})
|
||||
|
||||
// Check if it's a retryable error (5xx errors)
|
||||
if (error.status && error.status >= 500 && error.status < 600 && attempt < this.retryCount - 1) {
|
||||
// Exponential backoff
|
||||
const delay = this.retryDelay * Math.pow(2, attempt)
|
||||
console.log(`Retrying ChutesAI request after ${delay}ms...`)
|
||||
await new Promise((resolve) => setTimeout(resolve, delay))
|
||||
continue
|
||||
}
|
||||
|
||||
if (chunk.usage) {
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: chunk.usage.prompt_tokens || 0,
|
||||
outputTokens: chunk.usage.completion_tokens || 0,
|
||||
}
|
||||
}
|
||||
// For non-retryable errors or final attempt, throw with more context
|
||||
const enhancedError = new Error(
|
||||
`ChutesAI API error (${error.status || "unknown status"}): ${error.message || "Empty response body"}. ` +
|
||||
`This may be a temporary issue with the ChutesAI service. ` +
|
||||
`Please verify your API key and try again.`,
|
||||
)
|
||||
;(enhancedError as any).status = error.status
|
||||
;(enhancedError as any).originalError = error
|
||||
throw enhancedError
|
||||
}
|
||||
|
||||
// Process any remaining content
|
||||
for (const processedChunk of matcher.final()) {
|
||||
yield processedChunk
|
||||
}
|
||||
} else {
|
||||
yield* super.createMessage(systemPrompt, messages)
|
||||
}
|
||||
|
||||
// If we've exhausted all retries
|
||||
if (lastError) {
|
||||
throw lastError
|
||||
}
|
||||
}
|
||||
|
||||
override async completePrompt(prompt: string): Promise<string> {
|
||||
let lastError: Error | null = null
|
||||
|
||||
for (let attempt = 0; attempt < this.retryCount; attempt++) {
|
||||
try {
|
||||
return await super.completePrompt(prompt)
|
||||
} catch (error: any) {
|
||||
lastError = error
|
||||
console.error(`ChutesAI completePrompt error (attempt ${attempt + 1}/${this.retryCount}):`, {
|
||||
status: error.status,
|
||||
message: error.message,
|
||||
})
|
||||
|
||||
// Check if it's a retryable error (5xx errors)
|
||||
if (error.status && error.status >= 500 && error.status < 600 && attempt < this.retryCount - 1) {
|
||||
// Exponential backoff
|
||||
const delay = this.retryDelay * Math.pow(2, attempt)
|
||||
console.log(`Retrying ChutesAI completePrompt after ${delay}ms...`)
|
||||
await new Promise((resolve) => setTimeout(resolve, delay))
|
||||
continue
|
||||
}
|
||||
|
||||
// For non-retryable errors or final attempt, throw with more context
|
||||
const enhancedError = new Error(
|
||||
`ChutesAI completion error (${error.status || "unknown status"}): ${error.message || "Empty response body"}. ` +
|
||||
`Please verify your API key and endpoint configuration.`,
|
||||
)
|
||||
;(enhancedError as any).status = error.status
|
||||
;(enhancedError as any).originalError = error
|
||||
throw enhancedError
|
||||
}
|
||||
}
|
||||
|
||||
// If we've exhausted all retries
|
||||
if (lastError) {
|
||||
throw lastError
|
||||
}
|
||||
|
||||
throw new Error("ChutesAI completion failed after all retry attempts")
|
||||
}
|
||||
|
||||
override getModel() {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue