From 86d564967f8c76cb2185c6d3feea0bd82b2f1f08 Mon Sep 17 00:00:00 2001 From: viettranx Date: Wed, 15 Apr 2026 13:37:37 +0700 Subject: [PATCH] fix(agent/title): disable thinking and raise max_tokens for Gemini Gemini 2.5/3 default to high thinking via OpenAI-compat, consuming the entire max_tokens budget and truncating titles to a single word (and adding latency). Title generation is a trivial task that does not benefit from reasoning, so set OptThinkingLevel="off" and bump max_tokens from 50 to 256 for headroom across reasoning-capable providers. Also update mapGeminiReasoningEffort to forward "off" as "low" (the minimum effort all Gemini models accept via OpenAI-compat), since not forwarding causes the server to fall back to "high". --- internal/agent/title_generate.go | 11 +++++++++-- internal/providers/openai_gemini_thinking_test.go | 2 +- internal/providers/openai_request.go | 8 ++++++-- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/internal/agent/title_generate.go b/internal/agent/title_generate.go index 627a4534..eab223cf 100644 --- a/internal/agent/title_generate.go +++ b/internal/agent/title_generate.go @@ -26,8 +26,15 @@ func GenerateTitle(ctx context.Context, provider providers.Provider, model, user }, Model: model, Options: map[string]any{ - providers.OptMaxTokens: 50, - providers.OptTemperature: 0.3, + // Larger budget: thinking-capable models (Gemini 2.5/3, GPT-5 reasoning) + // can consume output tokens on reasoning traces. 256 leaves room for a + // 15-word title even when the provider allocates some budget to thinking. + providers.OptMaxTokens: 256, + providers.OptTemperature: 0.3, + // Disable extended thinking for title generation — it's a trivial task + // that doesn't benefit from reasoning and defaults (esp. Gemini's "high") + // otherwise eat the entire max_tokens budget, truncating the title to 1 word. + providers.OptThinkingLevel: "off", }, }) if err != nil { diff --git a/internal/providers/openai_gemini_thinking_test.go b/internal/providers/openai_gemini_thinking_test.go index b69669c4..c9380018 100644 --- a/internal/providers/openai_gemini_thinking_test.go +++ b/internal/providers/openai_gemini_thinking_test.go @@ -20,7 +20,7 @@ func TestBuildRequestBody_GeminiForwardsReasoningEffort(t *testing.T) { {"minimal_verbatim", "minimal", "minimal", true}, {"high_verbatim", "high", "high", true}, {"medium_maps_to_high", "medium", "high", true}, - {"off_omitted", "off", "", false}, + {"off_maps_to_low", "off", "low", true}, {"empty_omitted", "", "", false}, {"unknown_omitted", "garbage", "", false}, } diff --git a/internal/providers/openai_request.go b/internal/providers/openai_request.go index b3cac636..421be3e8 100644 --- a/internal/providers/openai_request.go +++ b/internal/providers/openai_request.go @@ -251,14 +251,18 @@ func (p *OpenAIProvider) isGeminiRoute(model string) bool { // 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. +// "off" maps to "low" (the minimum effort accepted by all Gemini models via +// OpenAI-compat). Forwarding is required because Gemini's default is "high", +// which consumes the entire max_tokens budget on reasoning traces and leaves +// no room for the response. Unknown values do not forward. func mapGeminiReasoningEffort(level string) (string, bool) { switch level { case "low", "minimal", "high": return level, true case "medium": return "high", true + case "off": + return "low", true default: return "", false }