From 2fcdbec22c9c573c544777749717e3a772d5dfd6 Mon Sep 17 00:00:00 2001 From: viettranx Date: Wed, 15 Apr 2026 12:37:43 +0700 Subject: [PATCH] fix(providers/openai): forward thinking_level to Gemini via reasoning_effort Gemini 3 defaults to "high" thinking budget internally when no reasoning_effort is set, consuming the full max_tokens budget and leaving no tokens for tool call arguments on small models. Map OptThinkingLevel to reasoning_effort on Gemini routes (native endpoint or proxy-by-model), with "medium" downgraded to "high" per Gemini 3 Preview constraint. Non-Gemini OpenAI-compat hosts unchanged. Trace: 019d8f33-2de1-7ab2-9a32-9df92cd610dd --- internal/providers/openai_request.go | 39 ++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/internal/providers/openai_request.go b/internal/providers/openai_request.go index 551dd58a..d152339d 100644 --- a/internal/providers/openai_request.go +++ b/internal/providers/openai_request.go @@ -188,6 +188,19 @@ func (p *OpenAIProvider) buildRequestBody(model string, req ChatRequest, stream } } + // Gemini (Google OpenAI-compat) accepts reasoning_effort mapped to thinking_config. + // Without forwarding, Gemini 3 defaults to "high" thinking and consumes the entire + // max_tokens budget, leaving no room for tool call arguments on small models. + // Gate narrowly: apiBase contains "generativelanguage" OR model substring "gemini" + // (covers OpenRouter / LiteLLM / Vertex proxies). + if _, already := body[OptReasoningEffort]; !already && p.isGeminiRoute(model) { + if level, ok := req.Options[OptThinkingLevel].(string); ok { + if mapped, forward := mapGeminiReasoningEffort(level); forward { + body[OptReasoningEffort] = mapped + } + } + } + // DashScope-specific passthrough keys — never send to other OpenAI-compat hosts. if p.dashScopePassthroughKeys() { if v, ok := req.Options[OptEnableThinking]; ok { @@ -219,6 +232,32 @@ func buildToolNameIndex(msgs []Message) map[string]string { return idx } +// isGeminiRoute returns true when this OpenAI-compat request targets Gemini, +// either via the native Google endpoint or a proxy (OpenRouter, LiteLLM) that +// routes by model string. Narrower than the supportsThoughtSignature gate — +// we require explicit intent before forwarding reasoning_effort on proxies. +func (p *OpenAIProvider) isGeminiRoute(model string) bool { + if strings.Contains(strings.ToLower(p.apiBase), "generativelanguage") { + return true + } + return strings.Contains(strings.ToLower(model), "gemini") +} + +// mapGeminiReasoningEffort returns (value, shouldForward). Gemini 3 Preview +// rejects "medium" with HTTP 400, so we map it to the nearest valid option. +// "off" and unknown values do not forward — respect user intent (off=disable) +// and avoid injecting defaults the model might otherwise apply conservatively. +func mapGeminiReasoningEffort(level string) (string, bool) { + switch level { + case "low", "minimal", "high": + return level, true + case "medium": + return "high", true + default: + return "", false + } +} + // modelFamily strips provider prefixes (for example "openai/o3-mini") so capability // gates apply to the actual model family rather than the transport-specific wrapper. func modelFamily(model string) string {