Skip to content
Merged
Show file tree
Hide file tree
Changes from 45 commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
5ff5993
feat: Adding more settings and control over Gemini
HahaBill Jun 19, 2025
afcb66d
feat: Adding parameter titles and descriptions + translation to all l…
HahaBill Jun 22, 2025
ac96e99
feat: adding more translations
HahaBill Jun 22, 2025
121e243
Merge branch 'RooCodeInc:main' into feat/finer-grained-control-gemini
HahaBill Jun 23, 2025
a20774e
Merge branch 'RooCodeInc:main' into feat/finer-grained-control-gemini
HahaBill Jun 23, 2025
67b4762
feat: adding `contextLimit` implementation from `maxContextWindow` PR…
HahaBill Jun 25, 2025
9595f76
feat: max value for context limit to model's limit + converting descr…
HahaBill Jun 25, 2025
26b1f53
feat: all languages translated
HahaBill Jun 26, 2025
8f468f4
feat: changing profile-specific threshold in context management setti…
HahaBill Jun 26, 2025
24e8ed5
Merge branch 'RooCodeInc:main' into feat/finer-grained-control-gemini
HahaBill Jun 26, 2025
98e813d
feat: max value of maxOutputTokens is model's maxTokens + adding more…
HahaBill Jun 26, 2025
91c16cb
feat: improve unit tests and adding `data-testid` to slider and check…
HahaBill Jun 26, 2025
1497edd
fix: small changes in geminiContextManagement descriptions + minor fix
HahaBill Jun 26, 2025
a204169
fix: Switching from "Gemini Context Management" to "Token Management
HahaBill Jun 29, 2025
83f02d5
fix: input field showed NaN -> annoying UX
HahaBill Jun 29, 2025
c438277
fix: Removing redundant "tokens" after the "set context limit"'s chec…
HahaBill Jul 2, 2025
f384f73
fix: Changing the translation to be consistent with the english one
HahaBill Jul 3, 2025
449a8c2
fix: more translations
HahaBill Jul 3, 2025
f8c04c9
fix: translations
HahaBill Jul 3, 2025
645b2fc
Merge branch 'RooCodeInc:main' into feat/finer-grained-control-gemini
HahaBill Jul 3, 2025
7e5a59d
fix: removing contextLimit and token management related code
HahaBill Jul 7, 2025
edb96c6
fix: removing `contextLimit` test and removing token management in tr…
HahaBill Jul 7, 2025
cae3de9
fix: changing from `Advanced Features` to `Tools` to be consistent wi…
HahaBill Jul 7, 2025
ae2e895
fix: adding `try-catch` block for `generateContentStream`
HahaBill Jul 8, 2025
a5f46b4
feat: Include citations + improved type safety
HahaBill Jul 8, 2025
bf01618
Merge branch 'RooCodeInc:main' into feat/finer-grained-control-gemini
HahaBill Jul 8, 2025
63c7b25
feat: adding citation for streams (generateContextStream)
HahaBill Jul 8, 2025
151601b
Merge branch 'RooCodeInc:main' into feat/finer-grained-control-gemini
HahaBill Jul 8, 2025
055bd79
fix: set default values for `topP`, `topK` and `maxOutputTokens`
HahaBill Jul 8, 2025
3ff4c1e
Merge branch 'RooCodeInc:main' into feat/finer-grained-control-gemini
HahaBill Jul 9, 2025
4200cff
fix: changing UI/UX according to the review/feedback from `daniel-lxs`
HahaBill Jul 9, 2025
0d72f08
fix: updating the `Gemini.spec.tsx` unit test
HahaBill Jul 9, 2025
d18b143
fix: more changes from the feedback/review from `daniel-lxs`
HahaBill Jul 9, 2025
22eb360
Merge branch 'RooCodeInc:main' into feat/finer-grained-control-gemini
HahaBill Jul 14, 2025
7e9d252
fix: adding sources at the end of the stream to preserve
HahaBill Jul 14, 2025
ae0a3b7
Merge branch 'RooCodeInc:main' into feat/finer-grained-control-gemini
HahaBill Jul 15, 2025
8d48fcc
Merge branch 'RooCodeInc:main' into feat/finer-grained-control-gemini
HahaBill Jul 16, 2025
d1386a5
Merge branch 'RooCodeInc:main' into feat/finer-grained-control-gemini
HahaBill Jul 17, 2025
8ca442e
fix: change the description for grounding with google search and url …
HahaBill Jul 17, 2025
a853329
Merge branch 'feat/finer-grained-control-gemini' of https://github.co…
HahaBill Jul 17, 2025
1c2aa36
fix: adding translations
HahaBill Jul 17, 2025
88a7eb4
fix: removing redundant extra translations - a mistake made by the agent
HahaBill Jul 18, 2025
847756c
fix: remove duplicate translation keys in geminiSections and geminiPa…
roomote Jul 18, 2025
3b8c801
fix: delete topK, topP and maxOutputTokens from Gemini
HahaBill Jul 19, 2025
728aded
fix: deleting topK, topP and maxOutputTokens from translations/locales
HahaBill Jul 19, 2025
9832b51
fix: adjust spacing between labels and descriptions + sentence casing
HahaBill Jul 19, 2025
abf3f1d
Merge branch 'RooCodeInc:main' into feat/adding-gemini-tools
HahaBill Jul 20, 2025
a6e8408
Merge branch 'RooCodeInc:main' into feat/adding-gemini-tools
HahaBill Jul 21, 2025
49a66d8
Merge remote-tracking branch 'origin/main' into feat/adding-gemini-tools
mrubens Jul 23, 2025
3ab7f13
Merge branch 'RooCodeInc:main' into feat/adding-gemini-tools
HahaBill Jul 23, 2025
b7b78df
Merge branch 'RooCodeInc:main' into feat/adding-gemini-tools
HahaBill Jul 23, 2025
5f63d15
Merge branch 'RooCodeInc:main' into feat/adding-gemini-tools
HahaBill Jul 24, 2025
c537802
Merge branch 'RooCodeInc:main' into feat/adding-gemini-tools
HahaBill Jul 25, 2025
8f21918
fix: adding maxOutputTokens back and removing unknown type
HahaBill Jul 25, 2025
5f8d0c2
fix: internalizing error Gemini error message
HahaBill Jul 25, 2025
5876976
fix: updating tests in Gemini and Vertex to adjust to the new error l…
HahaBill Jul 27, 2025
8fad431
Merge branch 'RooCodeInc:main' into feat/adding-gemini-tools
HahaBill Jul 27, 2025
816f634
fix: address PR review feedback for Gemini tools feature
daniel-lxs Jul 27, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/types/src/provider-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,8 @@ const lmStudioSchema = baseProviderSettingsSchema.extend({
const geminiSchema = apiModelIdProviderModelSchema.extend({
geminiApiKey: z.string().optional(),
googleGeminiBaseUrl: z.string().optional(),
enableUrlContext: z.boolean().optional(),
enableGrounding: z.boolean().optional(),
})

const geminiCliSchema = apiModelIdProviderModelSchema.extend({
Expand Down
36 changes: 36 additions & 0 deletions src/api/providers/__tests__/gemini-handler.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { describe, it, expect, vi } from "vitest"
import { GeminiHandler } from "../gemini"
import type { ApiHandlerOptions } from "../../../shared/api"

describe("GeminiHandler backend support", () => {
it("passes tools for URL context and grounding in config", async () => {
const options = {
apiProvider: "gemini",
enableUrlContext: true,
enableGrounding: true,
} as ApiHandlerOptions
const handler = new GeminiHandler(options)
const stub = vi.fn().mockReturnValue((async function* () {})())
// @ts-ignore access private client
handler["client"].models.generateContentStream = stub
await handler.createMessage("instr", [] as any).next()
const config = stub.mock.calls[0][0].config
expect(config.tools).toEqual([{ urlContext: {} }, { googleSearch: {} }])
})

it("completePrompt passes config overrides without tools when URL context and grounding disabled", async () => {
const options = {
apiProvider: "gemini",
enableUrlContext: false,
enableGrounding: false,
} as ApiHandlerOptions
const handler = new GeminiHandler(options)
const stub = vi.fn().mockResolvedValue({ text: "ok" })
// @ts-ignore access private client
handler["client"].models.generateContent = stub
const res = await handler.completePrompt("hi")
expect(res).toBe("ok")
const promptConfig = stub.mock.calls[0][0].config
expect(promptConfig.tools).toBeUndefined()
})
})
169 changes: 122 additions & 47 deletions src/api/providers/gemini.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
type GenerateContentResponseUsageMetadata,
type GenerateContentParameters,
type GenerateContentConfig,
type GroundingMetadata,
} from "@google/genai"
import type { JWTInput } from "google-auth-library"

Expand Down Expand Up @@ -67,72 +68,101 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl

const contents = messages.map(convertAnthropicMessageToGemini)

const config: GenerateContentConfig = {
const tools: GenerateContentConfig["tools"] = []
if (this.options.enableUrlContext) {
tools.push({ urlContext: {} })
}
if (this.options.enableGrounding) {
tools.push({ googleSearch: {} })
}
const rawConfig = {
systemInstruction,
httpOptions: this.options.googleGeminiBaseUrl ? { baseUrl: this.options.googleGeminiBaseUrl } : undefined,
thinkingConfig,
maxOutputTokens: this.options.modelMaxTokens ?? maxTokens ?? undefined,
temperature: this.options.modelTemperature ?? 0,
...(tools.length > 0 ? { tools } : {}),
}
const config = rawConfig as unknown as GenerateContentConfig

const params: GenerateContentParameters = { model, contents, config }

const result = await this.client.models.generateContentStream(params)
try {
const result = await this.client.models.generateContentStream(params)

let lastUsageMetadata: GenerateContentResponseUsageMetadata | undefined
let lastUsageMetadata: GenerateContentResponseUsageMetadata | undefined
let pendingGroundingMetadata: GroundingMetadata | undefined

for await (const chunk of result) {
// Process candidates and their parts to separate thoughts from content
if (chunk.candidates && chunk.candidates.length > 0) {
const candidate = chunk.candidates[0]
if (candidate.content && candidate.content.parts) {
for (const part of candidate.content.parts) {
if (part.thought) {
// This is a thinking/reasoning part
if (part.text) {
yield { type: "reasoning", text: part.text }
}
} else {
// This is regular content
if (part.text) {
yield { type: "text", text: part.text }
for await (const chunk of result) {
// Process candidates and their parts to separate thoughts from content
if (chunk.candidates && chunk.candidates.length > 0) {
const candidate = chunk.candidates[0]

if (candidate.groundingMetadata) {
pendingGroundingMetadata = candidate.groundingMetadata
}

if (candidate.content && candidate.content.parts) {
for (const part of candidate.content.parts) {
if (part.thought) {
// This is a thinking/reasoning part
if (part.text) {
yield { type: "reasoning", text: part.text }
}
} else {
// This is regular content
if (part.text) {
yield { type: "text", text: part.text }
}
}
}
}
}
}

// Fallback to the original text property if no candidates structure
else if (chunk.text) {
yield { type: "text", text: chunk.text }
// Fallback to the original text property if no candidates structure
else if (chunk.text) {
yield { type: "text", text: chunk.text }
}

if (chunk.usageMetadata) {
lastUsageMetadata = chunk.usageMetadata
}
}

if (chunk.usageMetadata) {
lastUsageMetadata = chunk.usageMetadata
if (pendingGroundingMetadata) {
const citations = this.extractCitationsOnly(pendingGroundingMetadata)
if (citations) {
yield { type: "text", text: `\n\nSources: ${citations}` }
}
}
}

if (lastUsageMetadata) {
const inputTokens = lastUsageMetadata.promptTokenCount ?? 0
const outputTokens = lastUsageMetadata.candidatesTokenCount ?? 0
const cacheReadTokens = lastUsageMetadata.cachedContentTokenCount
const reasoningTokens = lastUsageMetadata.thoughtsTokenCount

yield {
type: "usage",
inputTokens,
outputTokens,
cacheReadTokens,
reasoningTokens,
totalCost: this.calculateCost({ info, inputTokens, outputTokens, cacheReadTokens }),
if (lastUsageMetadata) {
const inputTokens = lastUsageMetadata.promptTokenCount ?? 0
const outputTokens = lastUsageMetadata.candidatesTokenCount ?? 0
const cacheReadTokens = lastUsageMetadata.cachedContentTokenCount
const reasoningTokens = lastUsageMetadata.thoughtsTokenCount

yield {
type: "usage",
inputTokens,
outputTokens,
cacheReadTokens,
reasoningTokens,
totalCost: this.calculateCost({ info, inputTokens, outputTokens, cacheReadTokens }),
}
}
} catch (error) {
if (error instanceof Error) {
throw new Error(`Gemini Generate Context Stream error: ${error.message}`)
}

throw error
}
}

override getModel() {
const modelId = this.options.apiModelId
let id = modelId && modelId in geminiModels ? (modelId as GeminiModelId) : geminiDefaultModelId
const info: ModelInfo = geminiModels[id]
let info: ModelInfo = geminiModels[id]
const params = getModelParams({ format: "gemini", modelId: id, model: info, settings: this.options })

// The `:thinking` suffix indicates that the model is a "Hybrid"
Expand All @@ -142,22 +172,67 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
return { id: id.endsWith(":thinking") ? id.replace(":thinking", "") : id, info, ...params }
}

private extractCitationsOnly(groundingMetadata?: GroundingMetadata): string | null {
const chunks = groundingMetadata?.groundingChunks

if (!chunks) {
return null
}

const citationLinks = chunks
.map((chunk, i) => {
const uri = chunk.web?.uri
if (uri) {
return `[${i + 1}](${uri})`
}
return null
})
.filter((link): link is string => link !== null)

if (citationLinks.length > 0) {
return citationLinks.join(", ")
}

return null
}

async completePrompt(prompt: string): Promise<string> {
try {
const { id: model } = this.getModel()

const tools: GenerateContentConfig["tools"] = []
if (this.options.enableUrlContext) {
tools.push({ urlContext: {} })
}
if (this.options.enableGrounding) {
tools.push({ googleSearch: {} })
}
const rawPromptConfig = {
httpOptions: this.options.googleGeminiBaseUrl
? { baseUrl: this.options.googleGeminiBaseUrl }
: undefined,
temperature: this.options.modelTemperature ?? 0,
...(tools.length > 0 ? { tools } : {}),
}
const promptConfig = rawPromptConfig as unknown as GenerateContentConfig

const result = await this.client.models.generateContent({
model,
contents: [{ role: "user", parts: [{ text: prompt }] }],
config: {
httpOptions: this.options.googleGeminiBaseUrl
? { baseUrl: this.options.googleGeminiBaseUrl }
: undefined,
temperature: this.options.modelTemperature ?? 0,
},
config: promptConfig,
})

return result.text ?? ""
let text = result.text ?? ""

const candidate = result.candidates?.[0]
if (candidate?.groundingMetadata) {
const citations = this.extractCitationsOnly(candidate.groundingMetadata)
if (citations) {
text += `\n\nSources: ${citations}`
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick, but should internationalize this

}
}

return text
} catch (error) {
if (error instanceof Error) {
throw new Error(`Gemini completion error: ${error.message}`)
Expand Down
1 change: 0 additions & 1 deletion src/core/sliding-window/__tests__/sliding-window.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,6 @@ describe("Sliding Window", () => {
{ role: "assistant", content: "Fourth message" },
{ role: "user", content: "Fifth message" },
]

it("should not truncate if tokens are below max tokens threshold", async () => {
const modelInfo = createModelInfo(100000, 30000)
const dynamicBuffer = modelInfo.contextWindow * TOKEN_BUFFER_PERCENTAGE // 10000
Expand Down
6 changes: 5 additions & 1 deletion webview-ui/src/components/settings/ApiOptions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -443,7 +443,11 @@ const ApiOptions = ({
)}

{selectedProvider === "gemini" && (
<Gemini apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
<Gemini
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
currentModelId={selectedModelId}
/>
)}

{selectedProvider === "openai" && (
Expand Down
25 changes: 24 additions & 1 deletion webview-ui/src/components/settings/providers/Gemini.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { inputEventTransform } from "../transforms"
type GeminiProps = {
apiConfiguration: ProviderSettings
setApiConfigurationField: (field: keyof ProviderSettings, value: ProviderSettings[keyof ProviderSettings]) => void
currentModelId?: string
}

export const Gemini = ({ apiConfiguration, setApiConfigurationField }: GeminiProps) => {
Expand Down Expand Up @@ -50,12 +51,13 @@ export const Gemini = ({ apiConfiguration, setApiConfigurationField }: GeminiPro
{t("settings:providers.getGeminiApiKey")}
</VSCodeButtonLink>
)}

<div>
<Checkbox
data-testid="checkbox-custom-base-url"
checked={googleGeminiBaseUrlSelected}
onChange={(checked: boolean) => {
setGoogleGeminiBaseUrlSelected(checked)

if (!checked) {
setApiConfigurationField("googleGeminiBaseUrl", "")
}
Expand All @@ -71,6 +73,27 @@ export const Gemini = ({ apiConfiguration, setApiConfigurationField }: GeminiPro
className="w-full mt-1"
/>
)}

<Checkbox
className="mt-6"
data-testid="checkbox-url-context"
checked={!!apiConfiguration.enableUrlContext}
onChange={(checked: boolean) => setApiConfigurationField("enableUrlContext", checked)}>
{t("settings:providers.geminiParameters.urlContext.title")}
</Checkbox>
<div className="text-sm text-vscode-descriptionForeground mb-3">
{t("settings:providers.geminiParameters.urlContext.description")}
</div>

<Checkbox
data-testid="checkbox-grounding-search"
checked={!!apiConfiguration.enableGrounding}
onChange={(checked: boolean) => setApiConfigurationField("enableGrounding", checked)}>
{t("settings:providers.geminiParameters.groundingSearch.title")}
</Checkbox>
<div className="text-sm text-vscode-descriptionForeground mb-3">
{t("settings:providers.geminiParameters.groundingSearch.description")}
</div>
</div>
</>
)
Expand Down
Loading