Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions .changeset/silly-cycles-enjoy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"roo-cline": patch
---

Adds support for minimal and medium reasoning effort levels in the Gemini provider implementation
196 changes: 196 additions & 0 deletions src/api/transform/__tests__/reasoning.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
OpenAiReasoningParams,
RooReasoningParams,
GeminiReasoningParams,
GeminiThinkingLevel,
} from "../reasoning"

describe("reasoning.ts", () => {
Expand Down Expand Up @@ -642,6 +643,201 @@ describe("reasoning.ts", () => {
const result = getGeminiReasoning(options) as GeminiReasoningParams | undefined
expect(result).toEqual({ thinkingLevel: "high", includeThoughts: true })
})

it("should return thinkingLevel for minimal effort", () => {
const geminiModel: ModelInfo = {
...baseModel,
supportsReasoningEffort: ["minimal", "low", "medium", "high"] as ModelInfo["supportsReasoningEffort"],
reasoningEffort: "high",
}

const settings: ProviderSettings = {
apiProvider: "gemini",
reasoningEffort: "minimal",
}

const options: GetModelReasoningOptions = {
model: geminiModel,
reasoningBudget: undefined,
reasoningEffort: "minimal",
settings,
}

const result = getGeminiReasoning(options) as GeminiReasoningParams | undefined
expect(result).toEqual({ thinkingLevel: "minimal", includeThoughts: true })
})

it("should return thinkingLevel for medium effort", () => {
const geminiModel: ModelInfo = {
...baseModel,
supportsReasoningEffort: ["minimal", "low", "medium", "high"] as ModelInfo["supportsReasoningEffort"],
reasoningEffort: "low",
}

const settings: ProviderSettings = {
apiProvider: "gemini",
reasoningEffort: "medium",
}

const options: GetModelReasoningOptions = {
model: geminiModel,
reasoningBudget: undefined,
reasoningEffort: "medium",
settings,
}

const result = getGeminiReasoning(options) as GeminiReasoningParams | undefined
expect(result).toEqual({ thinkingLevel: "medium", includeThoughts: true })
})

it("should handle all four Gemini thinking levels", () => {
const levels: GeminiThinkingLevel[] = ["minimal", "low", "medium", "high"]

levels.forEach((level) => {
const geminiModel: ModelInfo = {
...baseModel,
supportsReasoningEffort: [
"minimal",
"low",
"medium",
"high",
] as ModelInfo["supportsReasoningEffort"],
reasoningEffort: "low",
}

const settings: ProviderSettings = {
apiProvider: "gemini",
reasoningEffort: level,
}

const options: GetModelReasoningOptions = {
model: geminiModel,
reasoningBudget: undefined,
reasoningEffort: level,
settings,
}

const result = getGeminiReasoning(options) as GeminiReasoningParams | undefined
expect(result).toEqual({ thinkingLevel: level, includeThoughts: true })
})
})

it("should return undefined for disable effort", () => {
const geminiModel: ModelInfo = {
...baseModel,
supportsReasoningEffort: ["minimal", "low", "medium", "high"] as ModelInfo["supportsReasoningEffort"],
reasoningEffort: "low",
}

const settings: ProviderSettings = {
apiProvider: "gemini",
reasoningEffort: "disable",
}

const options: GetModelReasoningOptions = {
model: geminiModel,
reasoningBudget: undefined,
reasoningEffort: "disable",
settings,
}

const result = getGeminiReasoning(options)
expect(result).toBeUndefined()
})

it("should return undefined for none effort (invalid for Gemini)", () => {
const geminiModel: ModelInfo = {
...baseModel,
supportsReasoningEffort: ["minimal", "low", "medium", "high"] as ModelInfo["supportsReasoningEffort"],
reasoningEffort: "low",
}

const settings: ProviderSettings = {
apiProvider: "gemini",
reasoningEffort: "none",
}

const options: GetModelReasoningOptions = {
model: geminiModel,
reasoningBudget: undefined,
reasoningEffort: "none",
settings,
}

const result = getGeminiReasoning(options)
expect(result).toBeUndefined()
})

it("should use thinkingBudget for budget-based models", () => {
const geminiModel: ModelInfo = {
...baseModel,
supportsReasoningBudget: true,
requiredReasoningBudget: true,
}

const settings: ProviderSettings = {
apiProvider: "gemini",
enableReasoningEffort: true,
}

const options: GetModelReasoningOptions = {
model: geminiModel,
reasoningBudget: 4096,
reasoningEffort: "high",
settings,
}

const result = getGeminiReasoning(options) as GeminiReasoningParams | undefined
expect(result).toEqual({ thinkingBudget: 4096, includeThoughts: true })
})

it("should prioritize budget over effort when model has requiredReasoningBudget", () => {
const geminiModel: ModelInfo = {
...baseModel,
supportsReasoningBudget: true,
requiredReasoningBudget: true,
supportsReasoningEffort: ["minimal", "low", "medium", "high"] as ModelInfo["supportsReasoningEffort"],
}

const settings: ProviderSettings = {
apiProvider: "gemini",
enableReasoningEffort: true,
reasoningEffort: "high",
}

const options: GetModelReasoningOptions = {
model: geminiModel,
reasoningBudget: 8192,
reasoningEffort: "high",
settings,
}

const result = getGeminiReasoning(options) as GeminiReasoningParams | undefined
// Budget should take precedence
expect(result).toEqual({ thinkingBudget: 8192, includeThoughts: true })
})

it("should fall back to model default effort when settings.reasoningEffort is undefined", () => {
const geminiModel: ModelInfo = {
...baseModel,
supportsReasoningEffort: ["minimal", "low", "medium", "high"] as ModelInfo["supportsReasoningEffort"],
reasoningEffort: "medium",
}

const settings: ProviderSettings = {
apiProvider: "gemini",
}

const options: GetModelReasoningOptions = {
model: geminiModel,
reasoningBudget: undefined,
reasoningEffort: undefined,
settings,
}

const result = getGeminiReasoning(options) as GeminiReasoningParams | undefined
expect(result).toEqual({ thinkingLevel: "medium", includeThoughts: true })
})
})

describe("Integration scenarios", () => {
Expand Down
17 changes: 13 additions & 4 deletions src/api/transform/reasoning.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,17 @@ export type AnthropicReasoningParams = BetaThinkingConfigParam

export type OpenAiReasoningParams = { reasoning_effort: OpenAI.Chat.ChatCompletionCreateParams["reasoning_effort"] }

// Valid Gemini thinking levels for effort-based reasoning
const GEMINI_THINKING_LEVELS = ["minimal", "low", "medium", "high"] as const

export type GeminiThinkingLevel = (typeof GEMINI_THINKING_LEVELS)[number]

export function isGeminiThinkingLevel(value: unknown): value is GeminiThinkingLevel {
return typeof value === "string" && GEMINI_THINKING_LEVELS.includes(value as GeminiThinkingLevel)
}

export type GeminiReasoningParams = GenerateContentConfig["thinkingConfig"] & {
thinkingLevel?: "low" | "high"
thinkingLevel?: GeminiThinkingLevel
}

export type GetModelReasoningOptions = {
Expand Down Expand Up @@ -136,13 +145,13 @@ export const getGeminiReasoning = ({
| "disable"
| undefined

// Respect off / unset semantics from the effort selector itself.
// Respect "off" / unset semantics from the effort selector itself.
if (!selectedEffort || selectedEffort === "disable") {
return undefined
}

// Effort-based models on Google GenAI currently support only explicit low/high levels.
if (selectedEffort !== "low" && selectedEffort !== "high") {
// Effort-based models on Google GenAI support minimal/low/medium/high levels.
if (!isGeminiThinkingLevel(selectedEffort)) {
return undefined
}

Expand Down
Loading