feat: Add comprehensive error logging to Roo Cloud provider (#9098)

feat: add comprehensive error logging to Roo Cloud provider

- Add detailed error logging in handleOpenAIError() to capture error details before transformation
- Enhanced getRooModels() to log HTTP response details on failed requests
- Added error context logging to RooHandler streaming and model loading
- All existing tests passing (48 total)
This commit is contained in:
Daniel 2025-11-07 11:53:10 -05:00 committed by GitHub
parent 2abdad6d55
commit 247b06186f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 98 additions and 51 deletions

View file

@ -14,6 +14,11 @@ import { DEFAULT_HEADERS } from "../constants"
* @throws Will throw an error if the request fails or the response is not as expected.
*/
export async function getRooModels(baseUrl: string, apiKey?: string): Promise<ModelRecord> {
// Construct the models endpoint URL early so it's available in catch block for logging
// Strip trailing /v1 or /v1/ to avoid /v1/v1/models
const normalizedBase = baseUrl.replace(/\/?v1\/?$/, "")
const url = `${normalizedBase}/v1/models`
try {
const headers: Record<string, string> = {
"Content-Type": "application/json",
@ -24,11 +29,6 @@ export async function getRooModels(baseUrl: string, apiKey?: string): Promise<Mo
headers["Authorization"] = `Bearer ${apiKey}`
}
// Construct the models endpoint URL
// Strip trailing /v1 or /v1/ to avoid /v1/v1/models
const normalizedBase = baseUrl.replace(/\/?v1\/?$/, "")
const url = `${normalizedBase}/v1/models`
// Use fetch with AbortController for better timeout handling
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), 10000)
@ -40,6 +40,21 @@ export async function getRooModels(baseUrl: string, apiKey?: string): Promise<Mo
})
if (!response.ok) {
// Log detailed error information
let errorBody = ""
try {
errorBody = await response.text()
} catch {
errorBody = "(unable to read response body)"
}
console.error(`[getRooModels] HTTP error:`, {
status: response.status,
statusText: response.statusText,
url,
body: errorBody,
})
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
@ -105,7 +120,14 @@ export async function getRooModels(baseUrl: string, apiKey?: string): Promise<Mo
clearTimeout(timeoutId)
}
} catch (error: any) {
console.error("Error fetching Roo Code Cloud models:", error.message ? error.message : error)
// Enhanced error logging
console.error("[getRooModels] Error fetching Roo Code Cloud models:", {
message: error.message || String(error),
name: error.name,
stack: error.stack,
url,
hasApiKey: Boolean(apiKey),
})
// Handle abort/timeout
if (error.name === "AbortError") {

View file

@ -115,61 +115,72 @@ export class RooHandler extends BaseOpenAiCompatibleProvider<string> {
messages: Anthropic.Messages.MessageParam[],
metadata?: ApiHandlerCreateMessageMetadata,
): ApiStream {
const stream = await this.createStream(
systemPrompt,
messages,
metadata,
metadata?.taskId ? { headers: { "X-Roo-Task-ID": metadata.taskId } } : undefined,
)
try {
const stream = await this.createStream(
systemPrompt,
messages,
metadata,
metadata?.taskId ? { headers: { "X-Roo-Task-ID": metadata.taskId } } : undefined,
)
let lastUsage: RooUsage | undefined = undefined
let lastUsage: RooUsage | undefined = undefined
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) {
// Check for reasoning content (similar to OpenRouter)
if ("reasoning" in delta && delta.reasoning && typeof delta.reasoning === "string") {
yield {
type: "reasoning",
text: delta.reasoning,
if (delta) {
// Check for reasoning content (similar to OpenRouter)
if ("reasoning" in delta && delta.reasoning && typeof delta.reasoning === "string") {
yield {
type: "reasoning",
text: delta.reasoning,
}
}
// Also check for reasoning_content for backward compatibility
if ("reasoning_content" in delta && typeof delta.reasoning_content === "string") {
yield {
type: "reasoning",
text: delta.reasoning_content,
}
}
if (delta.content) {
yield {
type: "text",
text: delta.content,
}
}
}
// Also check for reasoning_content for backward compatibility
if ("reasoning_content" in delta && typeof delta.reasoning_content === "string") {
yield {
type: "reasoning",
text: delta.reasoning_content,
}
}
if (delta.content) {
yield {
type: "text",
text: delta.content,
}
if (chunk.usage) {
lastUsage = chunk.usage as RooUsage
}
}
if (chunk.usage) {
lastUsage = chunk.usage as RooUsage
}
}
if (lastUsage) {
// Check if the current model is marked as free
const model = this.getModel()
const isFreeModel = model.info.isFree ?? false
if (lastUsage) {
// Check if the current model is marked as free
const model = this.getModel()
const isFreeModel = model.info.isFree ?? false
yield {
type: "usage",
inputTokens: lastUsage.prompt_tokens || 0,
outputTokens: lastUsage.completion_tokens || 0,
cacheWriteTokens: lastUsage.cache_creation_input_tokens,
cacheReadTokens: lastUsage.prompt_tokens_details?.cached_tokens,
totalCost: isFreeModel ? 0 : (lastUsage.cost ?? 0),
yield {
type: "usage",
inputTokens: lastUsage.prompt_tokens || 0,
outputTokens: lastUsage.completion_tokens || 0,
cacheWriteTokens: lastUsage.cache_creation_input_tokens,
cacheReadTokens: lastUsage.prompt_tokens_details?.cached_tokens,
totalCost: isFreeModel ? 0 : (lastUsage.cost ?? 0),
}
}
} catch (error) {
// Log streaming errors with context
console.error("[RooHandler] Error during message streaming:", {
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
modelId: this.options.apiModelId,
hasTaskId: Boolean(metadata?.taskId),
})
throw error
}
}
override async completePrompt(prompt: string): Promise<string> {
@ -187,7 +198,13 @@ export class RooHandler extends BaseOpenAiCompatibleProvider<string> {
apiKey,
})
} catch (error) {
console.error("[RooHandler] Error loading dynamic models:", error)
// Enhanced error logging with more context
console.error("[RooHandler] Error loading dynamic models:", {
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
baseURL,
hasApiKey: Boolean(apiKey),
})
}
}

View file

@ -15,6 +15,13 @@ export function handleOpenAIError(error: unknown, providerName: string): Error {
if (error instanceof Error) {
const msg = error.message || ""
// Log the original error details for debugging
console.error(`[${providerName}] API error:`, {
message: msg,
name: error.name,
stack: error.stack,
})
// 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"))
@ -25,5 +32,6 @@ export function handleOpenAIError(error: unknown, providerName: string): Error {
}
// Non-Error: wrap with provider-specific prefix
console.error(`[${providerName}] Non-Error exception:`, error)
return new Error(`${providerName} completion error: ${String(error)}`)
}