mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
fix: handle reasoning-only responses from Gemini 2.5 Pro
- Add fallback text when Gemini 2.5 Pro returns only reasoning/thinking content - Prevents "no assistant messages" error for models with requiredReasoningBudget - Add comprehensive test coverage for reasoning-only and mixed content scenarios Fixes #6999
This commit is contained in:
parent
1d8b51d7c9
commit
e24d1aa7f1
2 changed files with 133 additions and 1 deletions
|
|
@ -90,6 +90,95 @@ describe("GeminiHandler", () => {
|
|||
)
|
||||
})
|
||||
|
||||
it("should handle reasoning-only responses for Gemini 2.5 Pro", async () => {
|
||||
// Mock a response with only reasoning/thinking content
|
||||
;(handler["client"].models.generateContentStream as any).mockResolvedValue({
|
||||
[Symbol.asyncIterator]: async function* () {
|
||||
yield {
|
||||
candidates: [
|
||||
{
|
||||
content: {
|
||||
parts: [
|
||||
{
|
||||
thought: true,
|
||||
text: "Let me think about this problem step by step...",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
yield { usageMetadata: { promptTokenCount: 10, candidatesTokenCount: 20, thoughtsTokenCount: 15 } }
|
||||
},
|
||||
})
|
||||
|
||||
const stream = handler.createMessage(systemPrompt, mockMessages)
|
||||
const chunks = []
|
||||
|
||||
for await (const chunk of stream) {
|
||||
chunks.push(chunk)
|
||||
}
|
||||
|
||||
// Should have reasoning chunk, text chunk (fallback), and usage
|
||||
expect(chunks.length).toBe(3)
|
||||
expect(chunks[0]).toEqual({ type: "reasoning", text: "Let me think about this problem step by step..." })
|
||||
expect(chunks[1]).toEqual({ type: "text", text: "[Thinking process completed]" })
|
||||
expect(chunks[2]).toEqual({
|
||||
type: "usage",
|
||||
inputTokens: 10,
|
||||
outputTokens: 20,
|
||||
cacheReadTokens: undefined,
|
||||
reasoningTokens: 15,
|
||||
totalCost: undefined,
|
||||
})
|
||||
})
|
||||
|
||||
it("should handle mixed reasoning and text content", async () => {
|
||||
// Mock a response with both reasoning and text content
|
||||
;(handler["client"].models.generateContentStream as any).mockResolvedValue({
|
||||
[Symbol.asyncIterator]: async function* () {
|
||||
yield {
|
||||
candidates: [
|
||||
{
|
||||
content: {
|
||||
parts: [
|
||||
{
|
||||
thought: true,
|
||||
text: "Analyzing the request...",
|
||||
},
|
||||
{
|
||||
text: "Here is my response",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
yield { usageMetadata: { promptTokenCount: 10, candidatesTokenCount: 25 } }
|
||||
},
|
||||
})
|
||||
|
||||
const stream = handler.createMessage(systemPrompt, mockMessages)
|
||||
const chunks = []
|
||||
|
||||
for await (const chunk of stream) {
|
||||
chunks.push(chunk)
|
||||
}
|
||||
|
||||
// Should have reasoning, text, and usage chunks (no fallback text needed)
|
||||
expect(chunks.length).toBe(3)
|
||||
expect(chunks[0]).toEqual({ type: "reasoning", text: "Analyzing the request..." })
|
||||
expect(chunks[1]).toEqual({ type: "text", text: "Here is my response" })
|
||||
expect(chunks[2]).toEqual({
|
||||
type: "usage",
|
||||
inputTokens: 10,
|
||||
outputTokens: 25,
|
||||
cacheReadTokens: undefined,
|
||||
reasoningTokens: undefined,
|
||||
totalCost: undefined,
|
||||
})
|
||||
})
|
||||
|
||||
it("should handle API errors", async () => {
|
||||
const mockError = new Error("Gemini API error")
|
||||
;(handler["client"].models.generateContentStream as any).mockRejectedValue(mockError)
|
||||
|
|
@ -143,6 +232,28 @@ describe("GeminiHandler", () => {
|
|||
const result = await handler.completePrompt("Test prompt")
|
||||
expect(result).toBe("")
|
||||
})
|
||||
|
||||
it("should handle reasoning-only response in completePrompt", async () => {
|
||||
// Mock a response with only reasoning/thinking content and no text
|
||||
;(handler["client"].models.generateContent as any).mockResolvedValue({
|
||||
text: "",
|
||||
candidates: [
|
||||
{
|
||||
content: {
|
||||
parts: [
|
||||
{
|
||||
thought: true,
|
||||
text: "Let me analyze this request...",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const result = await handler.completePrompt("Test prompt")
|
||||
expect(result).toBe("[Thinking process completed]")
|
||||
})
|
||||
})
|
||||
|
||||
describe("getModel", () => {
|
||||
|
|
|
|||
|
|
@ -94,6 +94,8 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
|
|||
|
||||
let lastUsageMetadata: GenerateContentResponseUsageMetadata | undefined
|
||||
let pendingGroundingMetadata: GroundingMetadata | undefined
|
||||
let hasTextContent = false
|
||||
let hasReasoningContent = false
|
||||
|
||||
for await (const chunk of result) {
|
||||
// Process candidates and their parts to separate thoughts from content
|
||||
|
|
@ -110,11 +112,13 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
|
|||
// This is a thinking/reasoning part
|
||||
if (part.text) {
|
||||
yield { type: "reasoning", text: part.text }
|
||||
hasReasoningContent = true
|
||||
}
|
||||
} else {
|
||||
// This is regular content
|
||||
if (part.text) {
|
||||
yield { type: "text", text: part.text }
|
||||
hasTextContent = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -124,6 +128,7 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
|
|||
// Fallback to the original text property if no candidates structure
|
||||
else if (chunk.text) {
|
||||
yield { type: "text", text: chunk.text }
|
||||
hasTextContent = true
|
||||
}
|
||||
|
||||
if (chunk.usageMetadata) {
|
||||
|
|
@ -131,6 +136,12 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
|
|||
}
|
||||
}
|
||||
|
||||
// If we only got reasoning content but no text content, yield a minimal text response
|
||||
// This prevents the "no assistant messages" error for Gemini 2.5 Pro with reasoning
|
||||
if (hasReasoningContent && !hasTextContent) {
|
||||
yield { type: "text", text: "[Thinking process completed]" }
|
||||
}
|
||||
|
||||
if (pendingGroundingMetadata) {
|
||||
const citations = this.extractCitationsOnly(pendingGroundingMetadata)
|
||||
if (citations) {
|
||||
|
|
@ -201,7 +212,7 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
|
|||
|
||||
async completePrompt(prompt: string): Promise<string> {
|
||||
try {
|
||||
const { id: model } = this.getModel()
|
||||
const { id: model, reasoning: thinkingConfig } = this.getModel()
|
||||
|
||||
const tools: GenerateContentConfig["tools"] = []
|
||||
if (this.options.enableUrlContext) {
|
||||
|
|
@ -215,6 +226,7 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
|
|||
? { baseUrl: this.options.googleGeminiBaseUrl }
|
||||
: undefined,
|
||||
temperature: this.options.modelTemperature ?? 0,
|
||||
thinkingConfig,
|
||||
...(tools.length > 0 ? { tools } : {}),
|
||||
}
|
||||
|
||||
|
|
@ -226,6 +238,15 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
|
|||
|
||||
let text = result.text ?? ""
|
||||
|
||||
// Handle case where model only returns reasoning/thinking content
|
||||
// This can happen with Gemini 2.5 Pro when reasoning is enabled
|
||||
if (!text && result.candidates?.[0]?.content?.parts) {
|
||||
const hasThoughts = result.candidates[0].content.parts.some((part: any) => part.thought)
|
||||
if (hasThoughts) {
|
||||
text = "[Thinking process completed]"
|
||||
}
|
||||
}
|
||||
|
||||
const candidate = result.candidates?.[0]
|
||||
if (candidate?.groundingMetadata) {
|
||||
const citations = this.extractCitationsOnly(candidate.groundingMetadata)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue