-
Notifications
You must be signed in to change notification settings - Fork 2.6k
Feat: Adding Gemini tools - URL Context and Grounding with Google Search #5959
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
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 afcb66d
feat: Adding parameter titles and descriptions + translation to all l…
HahaBill ac96e99
feat: adding more translations
HahaBill 121e243
Merge branch 'RooCodeInc:main' into feat/finer-grained-control-gemini
HahaBill a20774e
Merge branch 'RooCodeInc:main' into feat/finer-grained-control-gemini
HahaBill 67b4762
feat: adding `contextLimit` implementation from `maxContextWindow` PR…
HahaBill 9595f76
feat: max value for context limit to model's limit + converting descr…
HahaBill 26b1f53
feat: all languages translated
HahaBill 8f468f4
feat: changing profile-specific threshold in context management setti…
HahaBill 24e8ed5
Merge branch 'RooCodeInc:main' into feat/finer-grained-control-gemini
HahaBill 98e813d
feat: max value of maxOutputTokens is model's maxTokens + adding more…
HahaBill 91c16cb
feat: improve unit tests and adding `data-testid` to slider and check…
HahaBill 1497edd
fix: small changes in geminiContextManagement descriptions + minor fix
HahaBill a204169
fix: Switching from "Gemini Context Management" to "Token Management
HahaBill 83f02d5
fix: input field showed NaN -> annoying UX
HahaBill c438277
fix: Removing redundant "tokens" after the "set context limit"'s chec…
HahaBill f384f73
fix: Changing the translation to be consistent with the english one
HahaBill 449a8c2
fix: more translations
HahaBill f8c04c9
fix: translations
HahaBill 645b2fc
Merge branch 'RooCodeInc:main' into feat/finer-grained-control-gemini
HahaBill 7e5a59d
fix: removing contextLimit and token management related code
HahaBill edb96c6
fix: removing `contextLimit` test and removing token management in tr…
HahaBill cae3de9
fix: changing from `Advanced Features` to `Tools` to be consistent wi…
HahaBill ae2e895
fix: adding `try-catch` block for `generateContentStream`
HahaBill a5f46b4
feat: Include citations + improved type safety
HahaBill bf01618
Merge branch 'RooCodeInc:main' into feat/finer-grained-control-gemini
HahaBill 63c7b25
feat: adding citation for streams (generateContextStream)
HahaBill 151601b
Merge branch 'RooCodeInc:main' into feat/finer-grained-control-gemini
HahaBill 055bd79
fix: set default values for `topP`, `topK` and `maxOutputTokens`
HahaBill 3ff4c1e
Merge branch 'RooCodeInc:main' into feat/finer-grained-control-gemini
HahaBill 4200cff
fix: changing UI/UX according to the review/feedback from `daniel-lxs`
HahaBill 0d72f08
fix: updating the `Gemini.spec.tsx` unit test
HahaBill d18b143
fix: more changes from the feedback/review from `daniel-lxs`
HahaBill 22eb360
Merge branch 'RooCodeInc:main' into feat/finer-grained-control-gemini
HahaBill 7e9d252
fix: adding sources at the end of the stream to preserve
HahaBill ae0a3b7
Merge branch 'RooCodeInc:main' into feat/finer-grained-control-gemini
HahaBill 8d48fcc
Merge branch 'RooCodeInc:main' into feat/finer-grained-control-gemini
HahaBill d1386a5
Merge branch 'RooCodeInc:main' into feat/finer-grained-control-gemini
HahaBill 8ca442e
fix: change the description for grounding with google search and url …
HahaBill a853329
Merge branch 'feat/finer-grained-control-gemini' of https://github.co…
HahaBill 1c2aa36
fix: adding translations
HahaBill 88a7eb4
fix: removing redundant extra translations - a mistake made by the agent
HahaBill 847756c
fix: remove duplicate translation keys in geminiSections and geminiPa…
roomote 3b8c801
fix: delete topK, topP and maxOutputTokens from Gemini
HahaBill 728aded
fix: deleting topK, topP and maxOutputTokens from translations/locales
HahaBill 9832b51
fix: adjust spacing between labels and descriptions + sentence casing
HahaBill abf3f1d
Merge branch 'RooCodeInc:main' into feat/adding-gemini-tools
HahaBill a6e8408
Merge branch 'RooCodeInc:main' into feat/adding-gemini-tools
HahaBill 49a66d8
Merge remote-tracking branch 'origin/main' into feat/adding-gemini-tools
mrubens 3ab7f13
Merge branch 'RooCodeInc:main' into feat/adding-gemini-tools
HahaBill b7b78df
Merge branch 'RooCodeInc:main' into feat/adding-gemini-tools
HahaBill 5f63d15
Merge branch 'RooCodeInc:main' into feat/adding-gemini-tools
HahaBill c537802
Merge branch 'RooCodeInc:main' into feat/adding-gemini-tools
HahaBill 8f21918
fix: adding maxOutputTokens back and removing unknown type
HahaBill 5f8d0c2
fix: internalizing error Gemini error message
HahaBill 5876976
fix: updating tests in Gemini and Vertex to adjust to the new error l…
HahaBill 8fad431
Merge branch 'RooCodeInc:main' into feat/adding-gemini-tools
HahaBill 816f634
fix: address PR review feedback for Gemini tools feature
daniel-lxs File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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() | ||
| }) | ||
| }) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,7 @@ import { | |
| type GenerateContentResponseUsageMetadata, | ||
| type GenerateContentParameters, | ||
| type GenerateContentConfig, | ||
| type GroundingMetadata, | ||
| } from "@google/genai" | ||
| import type { JWTInput } from "google-auth-library" | ||
|
|
||
|
|
@@ -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 } : {}), | ||
| } | ||
daniel-lxs marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| const config = rawConfig as unknown as GenerateContentConfig | ||
daniel-lxs marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| 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}`) | ||
daniel-lxs marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| 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" | ||
|
|
@@ -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}` | ||
|
||
| } | ||
| } | ||
|
|
||
| return text | ||
| } catch (error) { | ||
| if (error instanceof Error) { | ||
| throw new Error(`Gemini completion error: ${error.message}`) | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.