diff --git a/litellm/__init__.py b/litellm/__init__.py index bc8a13ec2cd..a30a0b08d29 100644 --- a/litellm/__init__.py +++ b/litellm/__init__.py @@ -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 diff --git a/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py b/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py index ff51f1a013e..833e198f236 100644 --- a/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py +++ b/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py @@ -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 diff --git a/tests/test_litellm/llms/vertex_ai/gemini/test_vertex_and_google_ai_studio_gemini.py b/tests/test_litellm/llms/vertex_ai/gemini/test_vertex_and_google_ai_studio_gemini.py index 51cc2857252..9190018b6f1 100644 --- a/tests/test_litellm/llms/vertex_ai/gemini/test_vertex_and_google_ai_studio_gemini.py +++ b/tests/test_litellm/llms/vertex_ai/gemini/test_vertex_and_google_ai_studio_gemini.py @@ -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