Skip to content
Closed
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
43 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
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
6 changes: 6 additions & 0 deletions packages/types/src/provider-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,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({
Expand Down
72 changes: 72 additions & 0 deletions src/api/providers/__tests__/gemini-handler.spec.ts
Original file line number Diff line number Diff line change
@@ -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()
})
})
53 changes: 43 additions & 10 deletions src/api/providers/gemini.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Record<string, object>> = []
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 }

Expand Down Expand Up @@ -132,9 +144,16 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
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 })

if (this.options.contextLimit) {
info = {
...info,
contextWindow: this.options.contextLimit,
}
}

// The `:thinking` suffix indicates that the model is a "Hybrid"
// reasoning model and that reasoning is required to be enabled.
// The actual model ID honored by Gemini's API does not have this
Expand All @@ -146,15 +165,29 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
try {
const { id: model } = this.getModel()

const tools: Array<Record<string, object>> = []
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 ?? ""
Expand Down
24 changes: 24 additions & 0 deletions src/core/sliding-window/__tests__/sliding-window.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,30 @@ describe("Sliding Window", () => {
{ role: "assistant", content: "Fourth message" },
{ role: "user", content: "Fifth message" },
]
it("should use contextLimit as contextWindow when apiProvider is gemini", async () => {
const contextLimit = 2
const messages: ApiMessage[] = [
{ role: "user", content: "First message" },
{ role: "assistant", content: "Second message" },
{ role: "user", content: "Third message" },
{ role: "assistant", content: "Fourth message" },
{ role: "user", content: "" },
]
const result = await truncateConversationIfNeeded({
messages,
totalTokens: 2,
contextWindow: contextLimit,
maxTokens: null,
apiHandler: mockApiHandler,
autoCondenseContext: false,
autoCondenseContextPercent: 100,
systemPrompt: "",
taskId,
profileThresholds: {},
currentProfileId: "default",
})
expect(result.messages).toEqual([messages[0], messages[3], messages[4]])
})

it("should not truncate if tokens are below max tokens threshold", async () => {
const modelInfo = createModelInfo(100000, 30000)
Expand Down
5 changes: 4 additions & 1 deletion src/core/task/Task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1706,7 +1706,10 @@ export class Task extends EventEmitter<ClineEvents> {
? this.apiConfiguration.modelMaxTokens || DEFAULT_THINKING_MODEL_MAX_TOKENS
: modelInfo.maxTokens

const contextWindow = modelInfo.contextWindow
const contextWindow =
this.apiConfiguration.apiProvider === "gemini" && this.apiConfiguration.contextLimit
? this.apiConfiguration.contextLimit
: modelInfo.contextWindow

const truncateResult = await truncateConversationIfNeeded({
messages: this.apiConversationHistory,
Expand Down
18 changes: 17 additions & 1 deletion webview-ui/src/components/settings/ApiOptions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ export interface ApiOptionsProps {
fromWelcomeView?: boolean
errorMessage: string | undefined
setErrorMessage: React.Dispatch<React.SetStateAction<string | undefined>>
currentProfileId?: string
profileThresholds?: Record<string, number>
autoCondenseContextPercent?: number
setProfileThreshold?: (profileId: string, threshold: number) => void
}

const ApiOptions = ({
Expand All @@ -83,6 +87,10 @@ const ApiOptions = ({
fromWelcomeView,
errorMessage,
setErrorMessage,
currentProfileId,
profileThresholds,
autoCondenseContextPercent,
setProfileThreshold,
}: ApiOptionsProps) => {
const { t } = useAppTranslation()
const { organizationAllowList } = useExtensionState()
Expand Down Expand Up @@ -411,7 +419,15 @@ const ApiOptions = ({
)}

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

{selectedProvider === "openai" && (
Expand Down
23 changes: 23 additions & 0 deletions webview-ui/src/components/settings/SettingsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,15 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t

const apiConfiguration = useMemo(() => cachedState.apiConfiguration ?? {}, [cachedState.apiConfiguration])

const getCurrentProfileId = useCallback(() => {
if (!currentApiConfigName || !listApiConfigMeta) {
return currentApiConfigName
}

const profile = listApiConfigMeta.find((p) => p.name === currentApiConfigName)
return profile ? profile.id : currentApiConfigName
}, [currentApiConfigName, listApiConfigMeta])

useEffect(() => {
// Update only when currentApiConfigName is changed.
// Expected to be triggered by loadApiConfiguration/upsertApiConfiguration.
Expand Down Expand Up @@ -233,6 +242,16 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
})
}, [])

const setProfileThreshold = useCallback(
(profileId: string, threshold: number) => {
setCachedStateField("profileThresholds", {
...profileThresholds,
[profileId]: threshold,
})
},
[profileThresholds, setCachedStateField],
)

const setTelemetrySetting = useCallback((setting: TelemetrySetting) => {
setCachedState((prevState) => {
if (prevState.telemetrySetting === setting) {
Expand Down Expand Up @@ -576,6 +595,10 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
setApiConfigurationField={setApiConfigurationField}
errorMessage={errorMessage}
setErrorMessage={setErrorMessage}
currentProfileId={getCurrentProfileId()}
profileThresholds={profileThresholds || {}}
autoCondenseContextPercent={autoCondenseContextPercent || 75}
setProfileThreshold={setProfileThreshold}
/>
</Section>
</div>
Expand Down
Loading