Просмотр исходного кода

feat(gemini): add minimal and medium reasoning effort levels (#9973)

Co-authored-by: Roo Code <[email protected]>
Co-authored-by: cte <[email protected]>
Hannes Rudolph 3 недель назад
Родитель
Сommit
048e7f3502

+ 5 - 0
.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

+ 196 - 0
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", () => {

+ 13 - 4
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
 	}