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
This commit is contained in:
viettranx committed 2026-04-15 12:37:43 +07:00
1 parent a9b97fad2d
commit 2fcdbec22c
1 file changed
+39
+39
View File
@@ -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 {