diff --git a/internal/translator/gemini_helper.go b/internal/translator/gemini_helper.go index e126bfe4a4..9d9178f2b5 100644 --- a/internal/translator/gemini_helper.go +++ b/internal/translator/gemini_helper.go @@ -603,12 +603,21 @@ func isGeminiFlashModel(model internalapi.RequestModel) bool { return strings.Contains(strings.ToLower(model), "flash") } +// supportsMediumThinkingLevel reports whether the model accepts the "medium" thinking level. +// Gemini 3 Pro only lists "low" and "high", and Gemini 3.1 Flash-Lite Image only "minimal" and "high"; +// Flash and Gemini 3.1 Pro also list "medium". +// https://ai.google.dev/gemini-api/docs/thinking#thinking-levels +func supportsMediumThinkingLevel(model internalapi.RequestModel) bool { + m := strings.ToLower(model) + return !strings.Contains(m, "gemini-3-pro") && !strings.Contains(m, "flash-lite-image") +} + // mapReasoningEffortToThinkingLevel converts OpenAI reasoning effort levels to Gemini thinking levels. // The mapping depends on the model type: // - "none" → ThinkingLevelMinimal (Gemini Flash only) // - "low" → ThinkingLevelLow -// - "medium" → ThinkingLevelMedium for Flash, ThinkingLevelHigh for Pro -// - "high" → ThinkingLevelHigh +// - "medium" → ThinkingLevelMedium, except ThinkingLevelHigh for Gemini 3 Pro and 3.1 Flash-Lite Image, which have no medium level +// - "high" → ThinkingLevelHigh (every Gemini 3 model; it is the Pro default) // https://docs.cloud.google.com/vertex-ai/generative-ai/docs/start/get-started-with-gemini-3#openai-example func mapReasoningEffortToThinkingLevel(reasonEffort openai.ReasoningEffort, model internalapi.RequestModel) (genai.ThinkingLevel, error) { isFlash := isGeminiFlashModel(model) @@ -622,14 +631,11 @@ func mapReasoningEffortToThinkingLevel(reasonEffort openai.ReasoningEffort, mode case openai.ReasoningEffortLow: return genai.ThinkingLevelLow, nil case openai.ReasoningEffortMedium: - if isFlash { + if supportsMediumThinkingLevel(model) { return genai.ThinkingLevelMedium, nil } return genai.ThinkingLevelHigh, nil case openai.ReasoningEffortHigh: - if !isFlash { - return "", fmt.Errorf("%w: reasoning effort 'high' is only supported for Gemini Flash models", internalapi.ErrInvalidRequestBody) - } return genai.ThinkingLevelHigh, nil default: return "", fmt.Errorf("%w: unsupported reasoning effort level: %q (supported: none, low, medium, high)", internalapi.ErrInvalidRequestBody, reasonEffort) diff --git a/internal/translator/gemini_helper_test.go b/internal/translator/gemini_helper_test.go index 529efc726a..b7e3f3bd1e 100644 --- a/internal/translator/gemini_helper_test.go +++ b/internal/translator/gemini_helper_test.go @@ -1577,6 +1577,19 @@ func TestOpenAIReqToGeminiGenerationConfig(t *testing.T) { expectedResponseMode: responseModeNone, requestModel: "gemini-3-flash", }, + { + name: "high reasoning effort on Pro maps to ThinkingLevelHigh", + input: &openai.ChatCompletionRequest{ + ReasoningEffort: openai.ReasoningEffortHigh, + }, + expectedGenerationConfig: &genai.GenerationConfig{ + ThinkingConfig: &genai.ThinkingConfig{ + ThinkingLevel: genai.ThinkingLevelHigh, + }, + }, + expectedResponseMode: responseModeNone, + requestModel: "gemini-3-pro", + }, { name: "reasoning effort unsupported value", input: &openai.ChatCompletionRequest{ @@ -3448,17 +3461,41 @@ func TestMapReasoningEffortToThinkingLevel(t *testing.T) { expectedThinking: genai.ThinkingLevelMedium, }, { - name: "medium effort on Pro maps to ThinkingLevelHigh", + name: "medium effort on Gemini 3 Pro maps to ThinkingLevelHigh", reasoningEffort: openai.ReasoningEffortMedium, model: "gemini-3-pro", expectedThinking: genai.ThinkingLevelHigh, }, { - name: "high effort maps to ThinkingLevelHigh", + name: "medium effort on Gemini 3 Pro preview maps to ThinkingLevelHigh", + reasoningEffort: openai.ReasoningEffortMedium, + model: "gemini-3-pro-preview", + expectedThinking: genai.ThinkingLevelHigh, + }, + { + name: "medium effort on Gemini 3.1 Pro preview maps to ThinkingLevelMedium", + reasoningEffort: openai.ReasoningEffortMedium, + model: "gemini-3.1-pro-preview", + expectedThinking: genai.ThinkingLevelMedium, + }, + { + name: "medium effort on Gemini 3.1 Flash-Lite Image maps to ThinkingLevelHigh", + reasoningEffort: openai.ReasoningEffortMedium, + model: "gemini-3.1-flash-lite-image", + expectedThinking: genai.ThinkingLevelHigh, + }, + { + name: "high effort on Flash maps to ThinkingLevelHigh", reasoningEffort: openai.ReasoningEffortHigh, model: "gemini-3-flash", expectedThinking: genai.ThinkingLevelHigh, }, + { + name: "high effort on Pro maps to ThinkingLevelHigh", + reasoningEffort: openai.ReasoningEffortHigh, + model: "gemini-3-pro", + expectedThinking: genai.ThinkingLevelHigh, + }, { name: "empty effort - not supported", reasoningEffort: "",