mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
fix: unescape HTML entities in Gemini provider streaming responses
- Apply unescapeHtmlEntities to all text yielded from Gemini API - Fixes issue where square brackets were missing in Python/other code - Add comprehensive tests for HTML entity unescaping - Ensures consistency with other providers Fixes #8107
This commit is contained in:
parent
87b45def18
commit
d6713767b5
2 changed files with 152 additions and 4 deletions
|
|
@ -90,6 +90,133 @@ describe("GeminiHandler", () => {
|
|||
)
|
||||
})
|
||||
|
||||
it("should unescape HTML entities in text messages", async () => {
|
||||
// Setup the mock implementation to return text with HTML entities
|
||||
;(handler["client"].models.generateContentStream as any).mockResolvedValue({
|
||||
[Symbol.asyncIterator]: async function* () {
|
||||
yield { text: "array[0]" }
|
||||
yield { text: " and <div>" }
|
||||
yield { text: " with & symbol" }
|
||||
yield { usageMetadata: { promptTokenCount: 10, candidatesTokenCount: 5 } }
|
||||
},
|
||||
})
|
||||
|
||||
const stream = handler.createMessage(systemPrompt, mockMessages)
|
||||
const chunks = []
|
||||
|
||||
for await (const chunk of stream) {
|
||||
chunks.push(chunk)
|
||||
}
|
||||
|
||||
// Should have unescaped HTML entities
|
||||
expect(chunks[0]).toEqual({ type: "text", text: "array[0]" })
|
||||
expect(chunks[1]).toEqual({ type: "text", text: " and <div>" })
|
||||
expect(chunks[2]).toEqual({ type: "text", text: " with & symbol" })
|
||||
})
|
||||
|
||||
it("should unescape square bracket entities in Python code", async () => {
|
||||
// Test the exact scenario from the issue
|
||||
;(handler["client"].models.generateContentStream as any).mockResolvedValue({
|
||||
[Symbol.asyncIterator]: async function* () {
|
||||
yield {
|
||||
candidates: [
|
||||
{
|
||||
content: {
|
||||
parts: [
|
||||
{
|
||||
text: 'print(f"The first character is: {populated_variable[0]}")',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
yield { usageMetadata: { promptTokenCount: 10, candidatesTokenCount: 5 } }
|
||||
},
|
||||
})
|
||||
|
||||
const stream = handler.createMessage(systemPrompt, mockMessages)
|
||||
const chunks = []
|
||||
|
||||
for await (const chunk of stream) {
|
||||
chunks.push(chunk)
|
||||
}
|
||||
|
||||
// Should have properly unescaped square brackets
|
||||
expect(chunks[0]).toEqual({
|
||||
type: "text",
|
||||
text: 'print(f"The first character is: {populated_variable[0]}")',
|
||||
})
|
||||
})
|
||||
|
||||
it("should unescape named square bracket entities", async () => {
|
||||
// Test named entities for square brackets
|
||||
;(handler["client"].models.generateContentStream as any).mockResolvedValue({
|
||||
[Symbol.asyncIterator]: async function* () {
|
||||
yield {
|
||||
candidates: [
|
||||
{
|
||||
content: {
|
||||
parts: [
|
||||
{
|
||||
text: "matrix[i][j]",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
yield { usageMetadata: { promptTokenCount: 10, candidatesTokenCount: 5 } }
|
||||
},
|
||||
})
|
||||
|
||||
const stream = handler.createMessage(systemPrompt, mockMessages)
|
||||
const chunks = []
|
||||
|
||||
for await (const chunk of stream) {
|
||||
chunks.push(chunk)
|
||||
}
|
||||
|
||||
// Should have properly unescaped named entities
|
||||
expect(chunks[0]).toEqual({ type: "text", text: "matrix[i][j]" })
|
||||
})
|
||||
|
||||
it("should unescape HTML entities in thinking/reasoning parts", async () => {
|
||||
// Test that entities are unescaped in thinking parts too
|
||||
;(handler["client"].models.generateContentStream as any).mockResolvedValue({
|
||||
[Symbol.asyncIterator]: async function* () {
|
||||
yield {
|
||||
candidates: [
|
||||
{
|
||||
content: {
|
||||
parts: [
|
||||
{
|
||||
thought: true,
|
||||
text: "Need to access array[0] element",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
yield { usageMetadata: { promptTokenCount: 10, candidatesTokenCount: 5 } }
|
||||
},
|
||||
})
|
||||
|
||||
const stream = handler.createMessage(systemPrompt, mockMessages)
|
||||
const chunks = []
|
||||
|
||||
for await (const chunk of stream) {
|
||||
chunks.push(chunk)
|
||||
}
|
||||
|
||||
// Should have properly unescaped in reasoning text
|
||||
expect(chunks[0]).toEqual({
|
||||
type: "reasoning",
|
||||
text: "Need to access array[0] element",
|
||||
})
|
||||
})
|
||||
|
||||
it("should handle API errors", async () => {
|
||||
const mockError = new Error("Gemini API error")
|
||||
;(handler["client"].models.generateContentStream as any).mockRejectedValue(mockError)
|
||||
|
|
@ -143,6 +270,26 @@ describe("GeminiHandler", () => {
|
|||
const result = await handler.completePrompt("Test prompt")
|
||||
expect(result).toBe("")
|
||||
})
|
||||
|
||||
it("should unescape HTML entities in completePrompt response", async () => {
|
||||
// Mock the response with HTML entities
|
||||
;(handler["client"].models.generateContent as any).mockResolvedValue({
|
||||
text: "array[0] and <div>",
|
||||
})
|
||||
|
||||
const result = await handler.completePrompt("Test prompt")
|
||||
expect(result).toBe("array[0] and <div>")
|
||||
})
|
||||
|
||||
it("should handle square brackets in completePrompt for Python code", async () => {
|
||||
// Test the exact scenario from the issue
|
||||
;(handler["client"].models.generateContent as any).mockResolvedValue({
|
||||
text: 'print(f"The first character is: {populated_variable[0]}")',
|
||||
})
|
||||
|
||||
const result = await handler.completePrompt("Update Python script")
|
||||
expect(result).toBe('print(f"The first character is: {populated_variable[0]}")')
|
||||
})
|
||||
})
|
||||
|
||||
describe("getModel", () => {
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import { convertAnthropicContentToGemini, convertAnthropicMessageToGemini } from
|
|||
import { t } from "i18next"
|
||||
import type { ApiStream, GroundingSource } from "../transform/stream"
|
||||
import { getModelParams } from "../transform/model-params"
|
||||
import { unescapeHtmlEntities } from "../../utils/text-normalization"
|
||||
|
||||
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
|
||||
import { BaseProvider } from "./base-provider"
|
||||
|
|
@ -109,12 +110,12 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
|
|||
if (part.thought) {
|
||||
// This is a thinking/reasoning part
|
||||
if (part.text) {
|
||||
yield { type: "reasoning", text: part.text }
|
||||
yield { type: "reasoning", text: unescapeHtmlEntities(part.text) }
|
||||
}
|
||||
} else {
|
||||
// This is regular content
|
||||
if (part.text) {
|
||||
yield { type: "text", text: part.text }
|
||||
yield { type: "text", text: unescapeHtmlEntities(part.text) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -123,7 +124,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 }
|
||||
yield { type: "text", text: unescapeHtmlEntities(chunk.text) }
|
||||
}
|
||||
|
||||
if (chunk.usageMetadata) {
|
||||
|
|
@ -234,7 +235,7 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
|
|||
config: promptConfig,
|
||||
})
|
||||
|
||||
let text = result.text ?? ""
|
||||
let text = unescapeHtmlEntities(result.text ?? "")
|
||||
|
||||
const candidate = result.candidates?.[0]
|
||||
if (candidate?.groundingMetadata) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue