diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index 65e3f9b5b6..2238081ea4 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -151,6 +151,12 @@ const lmStudioSchema = baseProviderSettingsSchema.extend({ const geminiSchema = apiModelIdProviderModelSchema.extend({ geminiApiKey: z.string().optional(), googleGeminiBaseUrl: z.string().optional(), + topP: z.number().optional(), + topK: z.number().optional(), + maxOutputTokens: z.number().optional(), + enableUrlContext: z.boolean().optional(), + enableGrounding: z.boolean().optional(), + contextLimit: z.number().optional(), }) const openAiNativeSchema = apiModelIdProviderModelSchema.extend({ diff --git a/src/api/providers/__tests__/gemini-handler.spec.ts b/src/api/providers/__tests__/gemini-handler.spec.ts new file mode 100644 index 0000000000..2805593ca5 --- /dev/null +++ b/src/api/providers/__tests__/gemini-handler.spec.ts @@ -0,0 +1,72 @@ +import { describe, it, expect, vi } from "vitest" +import { GeminiHandler } from "../gemini" +import type { ApiHandlerOptions } from "../../../shared/api" +import type { Anthropic } from "@anthropic-ai/sdk" + +describe("GeminiHandler backend support", () => { + it("slices messages when contextLimit is set", async () => { + const options = { apiProvider: "gemini", contextLimit: 1 } as ApiHandlerOptions + const handler = new GeminiHandler(options) + const stub = vi.fn().mockReturnValue((async function* () {})()) + // @ts-ignore access private client + handler["client"].models.generateContentStream = stub + const messages = [ + { role: "user", content: [{ type: "text", text: "first" }] }, + { role: "assistant", content: [{ type: "text", text: "second" }] }, + ] as Anthropic.Messages.MessageParam[] + for await (const _ of handler.createMessage("instr", messages)) { + } + expect(stub).toHaveBeenCalledOnce() + const params = stub.mock.calls[0][0] + expect(params.contents).toHaveLength(1) + }) + + it("passes maxOutputTokens, topP, topK, and tools for URL context and grounding in config", async () => { + const options = { + apiProvider: "gemini", + maxOutputTokens: 5, + topP: 0.5, + topK: 10, + 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.maxOutputTokens).toBe(5) + expect(config.topP).toBe(0.5) + expect(config.topK).toBe(10) + expect(config.tools).toEqual([{ urlContext: {} }, { googleSearch: {} }]) + }) + + it("completePrompt passes config overrides without tools when URL context and grounding disabled", async () => { + const options = { + apiProvider: "gemini", + maxOutputTokens: 7, + topP: 0.7, + topK: 3, + 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") + expect(stub).toHaveBeenCalledWith( + expect.objectContaining({ + config: expect.objectContaining({ + maxOutputTokens: 7, + topP: 0.7, + topK: 3, + }), + }), + ) + const promptConfig = stub.mock.calls[0][0].config + expect(promptConfig.tools).toBeUndefined() + }) +}) diff --git a/src/api/providers/gemini.ts b/src/api/providers/gemini.ts index 6765c8676d..8790682f08 100644 --- a/src/api/providers/gemini.ts +++ b/src/api/providers/gemini.ts @@ -65,15 +65,27 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl ): ApiStream { const { id: model, info, reasoning: thinkingConfig, maxTokens } = this.getModel() - const contents = messages.map(convertAnthropicMessageToGemini) + const limitedMessages = this.options.contextLimit ? messages.slice(-this.options.contextLimit) : messages + const contents = limitedMessages.map(convertAnthropicMessageToGemini) - const config: GenerateContentConfig = { + const tools: Array> = [] + 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, + maxOutputTokens: this.options.maxOutputTokens ?? this.options.modelMaxTokens ?? maxTokens ?? undefined, temperature: this.options.modelTemperature ?? 0, + topP: this.options.topP, + topK: this.options.topK, + ...(tools.length > 0 ? { tools } : {}), } + const config = rawConfig as unknown as GenerateContentConfig const params: GenerateContentParameters = { model, contents, config } @@ -146,15 +158,29 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl try { const { id: model } = this.getModel() + const tools: Array> = [] + 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, + maxOutputTokens: this.options.maxOutputTokens ?? this.options.modelMaxTokens, + topP: this.options.topP, + topK: this.options.topK, + ...(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 ?? "" diff --git a/webview-ui/src/components/settings/providers/Gemini.tsx b/webview-ui/src/components/settings/providers/Gemini.tsx index 21056f12d5..04e8464f95 100644 --- a/webview-ui/src/components/settings/providers/Gemini.tsx +++ b/webview-ui/src/components/settings/providers/Gemini.tsx @@ -1,6 +1,7 @@ import { useCallback, useState } from "react" import { Checkbox } from "vscrui" import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react" +import { Slider } from "@src/components/ui" import type { ProviderSettings } from "@roo-code/types" @@ -72,6 +73,102 @@ export const Gemini = ({ apiConfiguration, setApiConfigurationField }: GeminiPro /> )} +
+ +
+ setApiConfigurationField("topP", values[0])} + className="flex-grow" + /> + {(apiConfiguration.topP ?? 0).toFixed(2)} +
+
+ {t("settings:providers.topPDescription")} +
+
+
+ +
+ setApiConfigurationField("topK", values[0])} + className="flex-grow" + /> + {apiConfiguration.topK ?? 0} +
+
+ {t("settings:providers.topKDescription")} +
+
+
+ +
+ setApiConfigurationField("maxOutputTokens", values[0])} + className="flex-grow" + /> + parseInt((e as any).target.value, 10))} + className="w-16" + /> +
+
+ {t("settings:providers.maxOutputTokensDescription")} +
+
+ setApiConfigurationField("enableUrlContext", checked)}> + {t("settings:providers.enableUrlContext")} + +
+ {t("settings:providers.enableUrlContextDescription")} +
+ setApiConfigurationField("enableGrounding", checked)}> + {t("settings:providers.enableGrounding")} + +
+ {t("settings:providers.enableGroundingDescription")} +
+
+ +
+ setApiConfigurationField("contextLimit", values[0])} + className="flex-grow" + /> + parseInt((e as any).target.value, 10))} + className="w-16" + /> +
+
+ {t("settings:providers.contextLimitDescription")} +
+
) }