mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
refactor: unify Gemini/Vertex error handling via handleAiSdkError() (#11364)
This commit is contained in:
parent
ff89965f59
commit
4272a69726
4 changed files with 121 additions and 38 deletions
|
|
@ -18,6 +18,7 @@ import {
|
|||
convertToolsForAiSdk,
|
||||
processAiSdkStreamPart,
|
||||
mapToolChoice,
|
||||
handleAiSdkError,
|
||||
} from "../transform/ai-sdk"
|
||||
import { t } from "i18next"
|
||||
import type { ApiStream, ApiStreamUsageChunk, GroundingSource } from "../transform/stream"
|
||||
|
|
@ -216,15 +217,14 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
|
|||
}
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
const apiError = new ApiProviderError(errorMessage, this.providerName, modelId, "createMessage")
|
||||
TelemetryService.instance.captureException(apiError)
|
||||
|
||||
if (error instanceof Error) {
|
||||
throw new Error(t("common:errors.gemini.generate_stream", { error: error.message }))
|
||||
}
|
||||
|
||||
throw error
|
||||
throw handleAiSdkError(error, this.providerName, {
|
||||
onError: (msg) => {
|
||||
TelemetryService.instance.captureException(
|
||||
new ApiProviderError(msg, this.providerName, modelId, "createMessage"),
|
||||
)
|
||||
},
|
||||
formatMessage: (msg) => t("common:errors.gemini.generate_stream", { error: msg }),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -364,15 +364,14 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
|
|||
|
||||
return text
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
const apiError = new ApiProviderError(errorMessage, this.providerName, modelId, "completePrompt")
|
||||
TelemetryService.instance.captureException(apiError)
|
||||
|
||||
if (error instanceof Error) {
|
||||
throw new Error(t("common:errors.gemini.generate_complete_prompt", { error: error.message }))
|
||||
}
|
||||
|
||||
throw error
|
||||
throw handleAiSdkError(error, this.providerName, {
|
||||
onError: (msg) => {
|
||||
TelemetryService.instance.captureException(
|
||||
new ApiProviderError(msg, this.providerName, modelId, "completePrompt"),
|
||||
)
|
||||
},
|
||||
formatMessage: (msg) => t("common:errors.gemini.generate_complete_prompt", { error: msg }),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import {
|
|||
convertToolsForAiSdk,
|
||||
processAiSdkStreamPart,
|
||||
mapToolChoice,
|
||||
handleAiSdkError,
|
||||
} from "../transform/ai-sdk"
|
||||
import { t } from "i18next"
|
||||
import type { ApiStream, ApiStreamUsageChunk, GroundingSource } from "../transform/stream"
|
||||
|
|
@ -200,15 +201,14 @@ export class VertexHandler extends BaseProvider implements SingleCompletionHandl
|
|||
throw usageError
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
const apiError = new ApiProviderError(errorMessage, this.providerName, modelId, "createMessage")
|
||||
TelemetryService.instance.captureException(apiError)
|
||||
|
||||
if (error instanceof Error) {
|
||||
throw new Error(t("common:errors.gemini.generate_stream", { error: error.message }))
|
||||
}
|
||||
|
||||
throw error
|
||||
throw handleAiSdkError(error, this.providerName, {
|
||||
onError: (msg) => {
|
||||
TelemetryService.instance.captureException(
|
||||
new ApiProviderError(msg, this.providerName, modelId, "createMessage"),
|
||||
)
|
||||
},
|
||||
formatMessage: (msg) => t("common:errors.gemini.generate_stream", { error: msg }),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -348,15 +348,14 @@ export class VertexHandler extends BaseProvider implements SingleCompletionHandl
|
|||
|
||||
return text
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
const apiError = new ApiProviderError(errorMessage, this.providerName, modelId, "completePrompt")
|
||||
TelemetryService.instance.captureException(apiError)
|
||||
|
||||
if (error instanceof Error) {
|
||||
throw new Error(t("common:errors.gemini.generate_complete_prompt", { error: error.message }))
|
||||
}
|
||||
|
||||
throw error
|
||||
throw handleAiSdkError(error, this.providerName, {
|
||||
onError: (msg) => {
|
||||
TelemetryService.instance.captureException(
|
||||
new ApiProviderError(msg, this.providerName, modelId, "completePrompt"),
|
||||
)
|
||||
},
|
||||
formatMessage: (msg) => t("common:errors.gemini.generate_complete_prompt", { error: msg }),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1055,6 +1055,55 @@ describe("AI SDK conversion utilities", () => {
|
|||
|
||||
expect((result as any).cause).toBe(originalError)
|
||||
})
|
||||
|
||||
it("should call onError with extracted message and original error", () => {
|
||||
const originalError = new Error("Quota exceeded")
|
||||
const onError = vi.fn()
|
||||
|
||||
handleAiSdkError(originalError, "Gemini", { onError })
|
||||
|
||||
expect(onError).toHaveBeenCalledOnce()
|
||||
expect(onError).toHaveBeenCalledWith("Quota exceeded", originalError)
|
||||
})
|
||||
|
||||
it("should use formatMessage to override default message format", () => {
|
||||
const error = new Error("Rate limit hit")
|
||||
const formatMessage = (msg: string) => `Custom: ${msg}`
|
||||
|
||||
const result = handleAiSdkError(error, "Vertex", { formatMessage })
|
||||
|
||||
expect(result.message).toBe("Custom: Rate limit hit")
|
||||
})
|
||||
|
||||
it("should call onError and use formatMessage together", () => {
|
||||
const originalError = {
|
||||
name: "AI_APICallError",
|
||||
message: "Forbidden",
|
||||
status: 403,
|
||||
}
|
||||
const onError = vi.fn()
|
||||
const formatMessage = (msg: string) => `Translated: ${msg}`
|
||||
|
||||
const result = handleAiSdkError(originalError, "Gemini", { onError, formatMessage })
|
||||
|
||||
// onError receives the extracted message
|
||||
expect(onError).toHaveBeenCalledOnce()
|
||||
expect(onError.mock.calls[0][0]).toContain("403")
|
||||
expect(onError.mock.calls[0][1]).toBe(originalError)
|
||||
|
||||
// formatMessage overrides the thrown message
|
||||
expect(result.message).toMatch(/^Translated:/)
|
||||
|
||||
// Status code is still preserved
|
||||
expect((result as any).status).toBe(403)
|
||||
})
|
||||
|
||||
it("should use default format when no options are provided", () => {
|
||||
const error = new Error("Something broke")
|
||||
const result = handleAiSdkError(error, "TestProvider")
|
||||
|
||||
expect(result.message).toBe("TestProvider: Something broke")
|
||||
})
|
||||
})
|
||||
|
||||
describe("extractMessageFromResponseBody", () => {
|
||||
|
|
|
|||
|
|
@ -769,17 +769,53 @@ function getStatusCode(obj: unknown): number | undefined {
|
|||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional configuration for `handleAiSdkError()` to support telemetry
|
||||
* capture and custom (e.g. i18n) message formatting without adding
|
||||
* direct dependencies to the shared transform layer.
|
||||
*/
|
||||
export interface HandleAiSdkErrorOptions {
|
||||
/**
|
||||
* Called with the extracted error message and the original error before
|
||||
* throwing. Use this to report to telemetry or structured logging.
|
||||
*
|
||||
* @example
|
||||
* onError: (msg) => {
|
||||
* TelemetryService.instance.captureException(
|
||||
* new ApiProviderError(msg, providerName, modelId, "createMessage"),
|
||||
* )
|
||||
* }
|
||||
*/
|
||||
onError?: (message: string, originalError: unknown) => void
|
||||
|
||||
/**
|
||||
* Custom message formatter. When provided, the returned string is used
|
||||
* as the thrown Error's message instead of the default
|
||||
* `${providerName}: ${extractedMessage}` format.
|
||||
*
|
||||
* @example
|
||||
* formatMessage: (msg) => t("common:errors.gemini.generate_stream", { error: msg })
|
||||
*/
|
||||
formatMessage?: (message: string) => string
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle AI SDK errors by extracting the message and preserving status codes.
|
||||
* Returns an Error object with proper status preserved for retry logic.
|
||||
*
|
||||
* @param error - The AI SDK error to handle
|
||||
* @param providerName - The name of the provider for context
|
||||
* @param options - Optional telemetry / i18n hooks (see {@link HandleAiSdkErrorOptions})
|
||||
* @returns An Error with preserved status code
|
||||
*/
|
||||
export function handleAiSdkError(error: unknown, providerName: string): Error {
|
||||
export function handleAiSdkError(error: unknown, providerName: string, options?: HandleAiSdkErrorOptions): Error {
|
||||
const message = extractAiSdkErrorMessage(error)
|
||||
const wrappedError = new Error(`${providerName}: ${message}`)
|
||||
|
||||
// Fire telemetry / logging callback before constructing the thrown error
|
||||
options?.onError?.(message, error)
|
||||
|
||||
const displayMessage = options?.formatMessage ? options.formatMessage(message) : `${providerName}: ${message}`
|
||||
const wrappedError = new Error(displayMessage)
|
||||
|
||||
// Preserve status code for retry logic
|
||||
const anyError = error as any
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue