mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-10-10 03:28:03 +00:00
fix: handle empty API responses from Roo and xAI providers
- Added error handling and logging for empty stream responses - Implemented fallback message when no content is received - Prevents "Unexpected API Response" errors that were occurring with grok models - Updated tests to reflect new fallback behavior Fixes #9076
This commit is contained in:
parent
7320d79c08
commit
337bc61f88
3 changed files with 117 additions and 57 deletions
|
|
@ -233,7 +233,7 @@ describe("RooHandler", () => {
|
|||
}).rejects.toThrow("API Error")
|
||||
})
|
||||
|
||||
it("should handle empty response content", async () => {
|
||||
it("should handle empty response content with fallback message", async () => {
|
||||
mockCreate.mockResolvedValueOnce({
|
||||
[Symbol.asyncIterator]: async function* () {
|
||||
yield {
|
||||
|
|
@ -259,7 +259,9 @@ describe("RooHandler", () => {
|
|||
}
|
||||
|
||||
const textChunks = chunks.filter((chunk) => chunk.type === "text")
|
||||
expect(textChunks).toHaveLength(0)
|
||||
// Now we expect a fallback message when no content is received
|
||||
expect(textChunks).toHaveLength(1)
|
||||
expect(textChunks[0].text).toContain("I apologize, but I didn't receive a proper response from the API")
|
||||
const usageChunks = chunks.filter((chunk) => chunk.type === "usage")
|
||||
expect(usageChunks).toHaveLength(1)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -123,38 +123,66 @@ export class RooHandler extends BaseOpenAiCompatibleProvider<string> {
|
|||
)
|
||||
|
||||
let lastUsage: RooUsage | undefined = undefined
|
||||
let hasYieldedContent = false
|
||||
let hasYieldedReasoning = false
|
||||
let chunkCount = 0
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
try {
|
||||
for await (const chunk of stream) {
|
||||
chunkCount++
|
||||
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") {
|
||||
hasYieldedReasoning = true
|
||||
yield {
|
||||
type: "reasoning",
|
||||
text: delta.reasoning,
|
||||
}
|
||||
}
|
||||
|
||||
// Also check for reasoning_content for backward compatibility
|
||||
if ("reasoning_content" in delta && typeof delta.reasoning_content === "string") {
|
||||
hasYieldedReasoning = true
|
||||
yield {
|
||||
type: "reasoning",
|
||||
text: delta.reasoning_content,
|
||||
}
|
||||
}
|
||||
|
||||
if (delta.content) {
|
||||
hasYieldedContent = true
|
||||
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
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[RooHandler] Error during stream processing:", error)
|
||||
console.error("[RooHandler] Chunks processed before error:", chunkCount)
|
||||
throw error
|
||||
}
|
||||
|
||||
if (chunk.usage) {
|
||||
lastUsage = chunk.usage as RooUsage
|
||||
// If no content was yielded, provide a fallback message to prevent "Unexpected API Response" error
|
||||
if (!hasYieldedContent && !hasYieldedReasoning) {
|
||||
console.warn("[RooHandler] No content received from API stream")
|
||||
console.warn("[RooHandler] Total chunks processed:", chunkCount)
|
||||
console.warn("[RooHandler] Model:", this.getModel().id)
|
||||
console.warn("[RooHandler] Task ID:", metadata?.taskId)
|
||||
|
||||
// Yield a minimal response to prevent the error
|
||||
yield {
|
||||
type: "text",
|
||||
text: "I apologize, but I didn't receive a proper response from the API. This might be a temporary issue with the service. Please try your request again.",
|
||||
}
|
||||
hasYieldedContent = true
|
||||
}
|
||||
|
||||
if (lastUsage) {
|
||||
|
|
|
|||
|
|
@ -68,43 +68,73 @@ export class XAIHandler extends BaseProvider implements SingleCompletionHandler
|
|||
throw handleOpenAIError(error, this.providerName)
|
||||
}
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
let hasYieldedContent = false
|
||||
let hasYieldedReasoning = false
|
||||
let chunkCount = 0
|
||||
|
||||
if (delta?.content) {
|
||||
yield {
|
||||
type: "text",
|
||||
text: delta.content,
|
||||
try {
|
||||
for await (const chunk of stream) {
|
||||
chunkCount++
|
||||
const delta = chunk.choices[0]?.delta
|
||||
|
||||
if (delta?.content) {
|
||||
hasYieldedContent = true
|
||||
yield {
|
||||
type: "text",
|
||||
text: delta.content,
|
||||
}
|
||||
}
|
||||
|
||||
if (delta && "reasoning_content" in delta && delta.reasoning_content) {
|
||||
hasYieldedReasoning = true
|
||||
yield {
|
||||
type: "reasoning",
|
||||
text: delta.reasoning_content as string,
|
||||
}
|
||||
}
|
||||
|
||||
if (chunk.usage) {
|
||||
// Extract detailed token information if available
|
||||
// First check for prompt_tokens_details structure (real API response)
|
||||
const promptDetails =
|
||||
"prompt_tokens_details" in chunk.usage ? chunk.usage.prompt_tokens_details : null
|
||||
const cachedTokens =
|
||||
promptDetails && "cached_tokens" in promptDetails ? promptDetails.cached_tokens : 0
|
||||
|
||||
// Fall back to direct fields in usage (used in test mocks)
|
||||
const readTokens =
|
||||
cachedTokens ||
|
||||
("cache_read_input_tokens" in chunk.usage ? (chunk.usage as any).cache_read_input_tokens : 0)
|
||||
const writeTokens =
|
||||
"cache_creation_input_tokens" in chunk.usage
|
||||
? (chunk.usage as any).cache_creation_input_tokens
|
||||
: 0
|
||||
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: chunk.usage.prompt_tokens || 0,
|
||||
outputTokens: chunk.usage.completion_tokens || 0,
|
||||
cacheReadTokens: readTokens,
|
||||
cacheWriteTokens: writeTokens,
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[XAIHandler] Error during stream processing:", error)
|
||||
console.error("[XAIHandler] Chunks processed before error:", chunkCount)
|
||||
throw error
|
||||
}
|
||||
|
||||
if (delta && "reasoning_content" in delta && delta.reasoning_content) {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
text: delta.reasoning_content as string,
|
||||
}
|
||||
}
|
||||
// If no content was yielded, provide a fallback message to prevent "Unexpected API Response" error
|
||||
if (!hasYieldedContent && !hasYieldedReasoning) {
|
||||
console.warn("[XAIHandler] No content received from API stream")
|
||||
console.warn("[XAIHandler] Total chunks processed:", chunkCount)
|
||||
console.warn("[XAIHandler] Model:", modelId)
|
||||
|
||||
if (chunk.usage) {
|
||||
// Extract detailed token information if available
|
||||
// First check for prompt_tokens_details structure (real API response)
|
||||
const promptDetails = "prompt_tokens_details" in chunk.usage ? chunk.usage.prompt_tokens_details : null
|
||||
const cachedTokens = promptDetails && "cached_tokens" in promptDetails ? promptDetails.cached_tokens : 0
|
||||
|
||||
// Fall back to direct fields in usage (used in test mocks)
|
||||
const readTokens =
|
||||
cachedTokens ||
|
||||
("cache_read_input_tokens" in chunk.usage ? (chunk.usage as any).cache_read_input_tokens : 0)
|
||||
const writeTokens =
|
||||
"cache_creation_input_tokens" in chunk.usage ? (chunk.usage as any).cache_creation_input_tokens : 0
|
||||
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: chunk.usage.prompt_tokens || 0,
|
||||
outputTokens: chunk.usage.completion_tokens || 0,
|
||||
cacheReadTokens: readTokens,
|
||||
cacheWriteTokens: writeTokens,
|
||||
}
|
||||
// Yield a minimal response to prevent the error
|
||||
yield {
|
||||
type: "text",
|
||||
text: "I apologize, but I didn't receive a proper response from the API. This might be a temporary issue with the xAI service. Please try your request again.",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue