diff --git a/.changeset/silly-cycles-enjoy.md b/.changeset/silly-cycles-enjoy.md new file mode 100644 index 00000000000..571b5253f6a --- /dev/null +++ b/.changeset/silly-cycles-enjoy.md @@ -0,0 +1,5 @@ +--- +"roo-cline": patch +--- + +Adds support for minimal and medium reasoning effort levels in the Gemini provider implementation diff --git a/src/api/transform/__tests__/reasoning.spec.ts b/src/api/transform/__tests__/reasoning.spec.ts index c1e6c6d5ce4..352aac8e7bb 100644 --- a/src/api/transform/__tests__/reasoning.spec.ts +++ b/src/api/transform/__tests__/reasoning.spec.ts @@ -14,6 +14,7 @@ import { OpenAiReasoningParams, RooReasoningParams, GeminiReasoningParams, + GeminiThinkingLevel, } from "../reasoning" describe("reasoning.ts", () => { @@ -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", () => { diff --git a/src/api/transform/reasoning.ts b/src/api/transform/reasoning.ts index f4940625696..e726ce32234 100644 --- a/src/api/transform/reasoning.ts +++ b/src/api/transform/reasoning.ts @@ -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 = { @@ -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 }