feat(gemini): allow disabling thought summaries while keeping thinking on

Both reasoning-effort mappers hardcoded includeThoughts to True whenever
thinking was on, so thinking ON with summaries OFF was unreachable even though
that is the google SDK's own default. Summaries are the narration rather than
the reasoning, and for callers that discard them they are pure added latency.

Adds litellm.gemini_include_thoughts_default, defaulting to True so nothing
changes for anyone who does not set it. It gates the includeThoughts=True
branches in the thinkingBudget mapper, the thinkingLevel mapper, and the
anthropic-style thinking param mapper.

The disable and none paths still force includeThoughts=False: with thinking
off there is nothing to summarise.
This commit is contained in:
Aryan Pardeshi 2026-08-11 01:54:51 +05:30
parent 9bca9dfbb1
commit c685b1b1bb
3 changed files with 84 additions and 12 deletions

View file

@ -371,6 +371,7 @@ enable_key_alias_format_validation: bool = (
enable_gemini_default_thinking_level_low: bool = (
False # opt-in: force thinkingLevel low/minimal for Gemini 3 thinking param mapping
)
gemini_include_thoughts_default: bool = True # set False to keep Gemini thinking on while dropping thought summaries
####################
logging: bool = True
enable_loadbalancing_on_batch_endpoints: Optional[bool] = None

View file

@ -793,6 +793,16 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig):
# - Requires propertyOrdering
optional_params["response_schema"] = self._map_response_schema(value=schema)
@staticmethod
def _include_thoughts() -> bool:
"""Whether to ask Gemini for thought summaries when thinking is on.
Thought summaries are the narration, not the reasoning: turning them off leaves
thinkingBudget/thinkingLevel untouched and the model still reasons, the response
just does not carry the summary back.
"""
return litellm.gemini_include_thoughts_default
@staticmethod
def _map_reasoning_effort_to_thinking_budget(
reasoning_effort: str,
@ -812,22 +822,22 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig):
return {
"thinkingBudget": budget,
"includeThoughts": True,
"includeThoughts": VertexGeminiConfig._include_thoughts(),
}
elif reasoning_effort == "low":
return {
"thinkingBudget": DEFAULT_REASONING_EFFORT_LOW_THINKING_BUDGET,
"includeThoughts": True,
"includeThoughts": VertexGeminiConfig._include_thoughts(),
}
elif reasoning_effort == "medium":
return {
"thinkingBudget": DEFAULT_REASONING_EFFORT_MEDIUM_THINKING_BUDGET,
"includeThoughts": True,
"includeThoughts": VertexGeminiConfig._include_thoughts(),
}
elif reasoning_effort == "high":
return {
"thinkingBudget": DEFAULT_REASONING_EFFORT_HIGH_THINKING_BUDGET,
"includeThoughts": True,
"includeThoughts": VertexGeminiConfig._include_thoughts(),
}
elif reasoning_effort == "disable":
return {
@ -863,18 +873,18 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig):
is_gemini31pro: Final = model and ("gemini-3.1-pro-preview" in model.lower())
if reasoning_effort == "minimal":
if is_gemini3flash:
return {"thinkingLevel": "minimal", "includeThoughts": True}
return {"thinkingLevel": "minimal", "includeThoughts": VertexGeminiConfig._include_thoughts()}
else:
return {"thinkingLevel": "low", "includeThoughts": True}
return {"thinkingLevel": "low", "includeThoughts": VertexGeminiConfig._include_thoughts()}
elif reasoning_effort == "low":
return {"thinkingLevel": "low", "includeThoughts": True}
return {"thinkingLevel": "low", "includeThoughts": VertexGeminiConfig._include_thoughts()}
elif reasoning_effort == "medium":
if is_gemini31pro or is_gemini3flash:
return {"thinkingLevel": "medium", "includeThoughts": True}
return {"thinkingLevel": "medium", "includeThoughts": VertexGeminiConfig._include_thoughts()}
else:
return {"thinkingLevel": "high", "includeThoughts": True}
return {"thinkingLevel": "high", "includeThoughts": VertexGeminiConfig._include_thoughts()}
elif reasoning_effort == "high":
return {"thinkingLevel": "high", "includeThoughts": True}
return {"thinkingLevel": "high", "includeThoughts": VertexGeminiConfig._include_thoughts()}
elif reasoning_effort == "disable":
# Gemini 3 cannot fully disable thinking, so we use "minimal" for gemini-3-flash-preview, "low" for others
if is_gemini3flash:
@ -950,7 +960,7 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig):
if thinking_budget is None or thinking_budget == 0:
params["includeThoughts"] = False
else:
params["includeThoughts"] = True
params["includeThoughts"] = VertexGeminiConfig._include_thoughts()
# Follow provider defaults unless explicitly opted into legacy behavior.
if litellm.enable_gemini_default_thinking_level_low is True:
is_gemini3flash: Final = "gemini-3" in model.lower() and "flash" in model.lower()
@ -961,7 +971,7 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig):
else:
# For older Gemini models, use thinkingBudget
if thinking_enabled and not VertexGeminiConfig._is_thinking_budget_zero(thinking_budget):
params["includeThoughts"] = True
params["includeThoughts"] = VertexGeminiConfig._include_thoughts()
if thinking_budget is not None and isinstance(thinking_budget, int):
params["thinkingBudget"] = thinking_budget

View file

@ -5553,3 +5553,64 @@ def test_accumulated_json_skips_non_dict_leading_value():
assert len(out) == 1
assert out[0].choices[0].delta.content == "a"
@pytest.fixture
def gemini_thought_summaries_off():
original = litellm.gemini_include_thoughts_default
litellm.gemini_include_thoughts_default = False
try:
yield
finally:
litellm.gemini_include_thoughts_default = original
@pytest.mark.parametrize("reasoning_effort", ["minimal", "low", "medium", "high"])
def test_thinking_budget_includes_thoughts_by_default(reasoning_effort):
config = VertexGeminiConfig._map_reasoning_effort_to_thinking_budget(
reasoning_effort=reasoning_effort, model="gemini-2.5-pro"
)
assert config["includeThoughts"] is True
@pytest.mark.parametrize("reasoning_effort", ["minimal", "low", "medium", "high"])
def test_thinking_budget_can_drop_thought_summaries_while_still_thinking(
reasoning_effort, gemini_thought_summaries_off
):
config = VertexGeminiConfig._map_reasoning_effort_to_thinking_budget(
reasoning_effort=reasoning_effort, model="gemini-2.5-pro"
)
assert config["includeThoughts"] is False
assert config["thinkingBudget"] > 0
@pytest.mark.parametrize("reasoning_effort", ["minimal", "low", "medium", "high"])
def test_thinking_level_can_drop_thought_summaries_while_still_thinking(
reasoning_effort, gemini_thought_summaries_off
):
config = VertexGeminiConfig._map_reasoning_effort_to_thinking_level(
reasoning_effort=reasoning_effort, model="gemini-3-pro-preview"
)
assert config["includeThoughts"] is False
assert config["thinkingLevel"] in ("minimal", "low", "medium", "high")
@pytest.mark.parametrize("reasoning_effort", ["disable", "none"])
def test_disabling_thinking_still_disables_thought_summaries(reasoning_effort):
budget_config = VertexGeminiConfig._map_reasoning_effort_to_thinking_budget(
reasoning_effort=reasoning_effort, model="gemini-2.5-pro"
)
level_config = VertexGeminiConfig._map_reasoning_effort_to_thinking_level(
reasoning_effort=reasoning_effort, model="gemini-3-pro-preview"
)
assert budget_config["includeThoughts"] is False
assert level_config["includeThoughts"] is False
def test_thinking_param_can_drop_thought_summaries(gemini_thought_summaries_off):
config = VertexGeminiConfig._map_thinking_param(
thinking_param={"type": "enabled", "budget_tokens": 2048},
model="gemini-2.5-pro",
)
assert config["includeThoughts"] is False
assert config["thinkingBudget"] == 2048