Skip to content

Commit 67f0f0e

Browse files
committed
fix: disable Gemini grounding for code generation to prevent array index removal
When Gemini grounding is enabled, it misinterprets array indices like [0] as citation markers and removes them from generated code. This fix detects code generation contexts and temporarily disables grounding to preserve array indices. Fixes #8914
1 parent ff0c65a commit 67f0f0e

File tree

2 files changed

+196
-2
lines changed

2 files changed

+196
-2
lines changed

src/api/providers/__tests__/gemini.spec.ts

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,145 @@ describe("GeminiHandler", () => {
164164
})
165165
})
166166

167+
describe("grounding bypass for code generation", () => {
168+
it("should disable grounding when code generation context is detected", async () => {
169+
const codeHandler = new GeminiHandler({
170+
apiModelId: GEMINI_20_FLASH_THINKING_NAME,
171+
geminiApiKey: "test-key",
172+
enableGrounding: true, // Grounding is enabled
173+
})
174+
175+
// Mock the client's generateContentStream method
176+
const mockGenerateContentStream = vi.fn().mockResolvedValue({
177+
[Symbol.asyncIterator]: async function* () {
178+
yield {
179+
text: "fruits = ['apple', 'banana']\nmyfruit = fruits[0]",
180+
usageMetadata: { promptTokenCount: 100, candidatesTokenCount: 50 },
181+
}
182+
},
183+
})
184+
codeHandler["client"].models.generateContentStream = mockGenerateContentStream
185+
186+
// Test message that includes code generation request
187+
const messages: Anthropic.Messages.MessageParam[] = [
188+
{
189+
role: "user",
190+
content: [
191+
{
192+
type: "text",
193+
text: "Create a python file that creates a list of 10 fruits and put the first one in the variable 'myfruit'",
194+
},
195+
],
196+
},
197+
]
198+
199+
const stream = codeHandler.createMessage("System prompt", messages)
200+
const chunks = []
201+
for await (const chunk of stream) {
202+
chunks.push(chunk)
203+
}
204+
205+
// Verify that googleSearch was NOT added to tools (grounding disabled)
206+
const callArgs = mockGenerateContentStream.mock.calls[0][0]
207+
const tools = callArgs.config.tools || []
208+
const hasGrounding = tools.some((tool: any) => "googleSearch" in tool)
209+
expect(hasGrounding).toBe(false)
210+
})
211+
212+
it("should enable grounding for non-code contexts when enableGrounding is true", async () => {
213+
const nonCodeHandler = new GeminiHandler({
214+
apiModelId: GEMINI_20_FLASH_THINKING_NAME,
215+
geminiApiKey: "test-key",
216+
enableGrounding: true,
217+
})
218+
219+
// Mock the client's generateContentStream method
220+
const mockGenerateContentStream = vi.fn().mockResolvedValue({
221+
[Symbol.asyncIterator]: async function* () {
222+
yield {
223+
text: "The weather today is sunny.",
224+
usageMetadata: { promptTokenCount: 100, candidatesTokenCount: 50 },
225+
}
226+
},
227+
})
228+
nonCodeHandler["client"].models.generateContentStream = mockGenerateContentStream
229+
230+
// Test message without code generation context
231+
const messages: Anthropic.Messages.MessageParam[] = [
232+
{
233+
role: "user",
234+
content: [
235+
{
236+
type: "text",
237+
text: "What's the weather like today?",
238+
},
239+
],
240+
},
241+
]
242+
243+
const stream = nonCodeHandler.createMessage("System prompt", messages)
244+
const chunks = []
245+
for await (const chunk of stream) {
246+
chunks.push(chunk)
247+
}
248+
249+
// Verify that googleSearch WAS added to tools (grounding enabled)
250+
const callArgs = mockGenerateContentStream.mock.calls[0][0]
251+
const tools = callArgs.config.tools || []
252+
const hasGrounding = tools.some((tool: any) => "googleSearch" in tool)
253+
expect(hasGrounding).toBe(true)
254+
})
255+
256+
it("should correctly identify various code generation patterns", async () => {
257+
const handler = new GeminiHandler({
258+
apiModelId: GEMINI_20_FLASH_THINKING_NAME,
259+
geminiApiKey: "test-key",
260+
})
261+
262+
// Test various code generation patterns
263+
const codePatterns = [
264+
"Create a python file with a list",
265+
"Write a javascript function",
266+
"Generate code snippet",
267+
"Implement a class method",
268+
"def my_function():",
269+
"function getData() {",
270+
"fruits[0] = 'apple'",
271+
"array[5]",
272+
]
273+
274+
for (const pattern of codePatterns) {
275+
const messages: Anthropic.Messages.MessageParam[] = [
276+
{
277+
role: "user",
278+
content: [{ type: "text", text: pattern }],
279+
},
280+
]
281+
const result = handler["isCodeGenerationContext"](messages)
282+
expect(result).toBe(true)
283+
}
284+
285+
// Test non-code patterns
286+
const nonCodePatterns = [
287+
"What's the weather?",
288+
"Explain quantum physics",
289+
"Tell me a story",
290+
"How do I cook pasta?",
291+
]
292+
293+
for (const pattern of nonCodePatterns) {
294+
const messages: Anthropic.Messages.MessageParam[] = [
295+
{
296+
role: "user",
297+
content: [{ type: "text", text: pattern }],
298+
},
299+
]
300+
const result = handler["isCodeGenerationContext"](messages)
301+
expect(result).toBe(false)
302+
}
303+
})
304+
})
305+
167306
describe("calculateCost", () => {
168307
// Mock ModelInfo based on gemini-1.5-flash-latest pricing (per 1M tokens)
169308
// Removed 'id' and 'name' as they are not part of ModelInfo type directly

src/api/providers/gemini.ts

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,52 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
6060
: new GoogleGenAI({ apiKey })
6161
}
6262

63+
/**
64+
* Detects if the conversation context suggests code generation.
65+
* This helps prevent Gemini's grounding feature from incorrectly
66+
* removing array indices like [0] which it may interpret as citations.
67+
*/
68+
private isCodeGenerationContext(messages: Anthropic.Messages.MessageParam[]): boolean {
69+
// Keywords that strongly suggest code generation
70+
const codeKeywords = [
71+
"create.*(?:file|script|function|class|method|code|program)",
72+
"write.*(?:file|script|function|class|method|code|program)",
73+
"generate.*(?:file|script|function|class|method|code|program)",
74+
"implement.*(?:function|class|method|algorithm)",
75+
"python file",
76+
"javascript file",
77+
"typescript file",
78+
"code snippet",
79+
"code example",
80+
"def\\s+\\w+\\s*\\(", // Python function definition
81+
"function\\s+\\w+\\s*\\(", // JavaScript function
82+
"class\\s+\\w+", // Class definition
83+
"\\[\\d+\\]", // Array index patterns
84+
"array\\[",
85+
"list\\[",
86+
"fruits\\[0\\]", // Specific to the reported issue
87+
]
88+
89+
const codePattern = new RegExp(codeKeywords.join("|"), "i")
90+
91+
// Check recent messages for code-related content
92+
const recentMessages = messages.slice(-5) // Check last 5 messages
93+
94+
for (const message of recentMessages) {
95+
if (Array.isArray(message.content)) {
96+
for (const block of message.content) {
97+
if (block.type === "text" && codePattern.test(block.text)) {
98+
return true
99+
}
100+
}
101+
} else if (typeof message.content === "string" && codePattern.test(message.content)) {
102+
return true
103+
}
104+
}
105+
106+
return false
107+
}
108+
63109
async *createMessage(
64110
systemInstruction: string,
65111
messages: Anthropic.Messages.MessageParam[],
@@ -74,7 +120,10 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
74120
tools.push({ urlContext: {} })
75121
}
76122

77-
if (this.options.enableGrounding) {
123+
// Only enable grounding if it's not a code generation context
124+
// This prevents Gemini from misinterpreting array indices like [0] as citation markers
125+
const isCodeContext = this.isCodeGenerationContext(messages)
126+
if (this.options.enableGrounding && !isCodeContext) {
78127
tools.push({ googleSearch: {} })
79128
}
80129

@@ -217,7 +266,13 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
217266
if (this.options.enableUrlContext) {
218267
tools.push({ urlContext: {} })
219268
}
220-
if (this.options.enableGrounding) {
269+
270+
// Check if the prompt suggests code generation
271+
const isCodeContext = this.isCodeGenerationContext([
272+
{ role: "user", content: [{ type: "text", text: prompt }] },
273+
])
274+
275+
if (this.options.enableGrounding && !isCodeContext) {
221276
tools.push({ googleSearch: {} })
222277
}
223278
const promptConfig: GenerateContentConfig = {

0 commit comments

Comments
 (0)