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 }