Merge pull request #26077 from BerriAI/litellm_vertex_cached_content_mutual_exclusion

fix(vertex_ai): omit system_instruction/tools/toolConfig when cachedContent set
This commit is contained in:
Mateo Wang 2026-05-01 15:52:47 -07:00 committed by GitHub
commit 6458fe15fd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 86 additions and 10 deletions

View file

@ -743,16 +743,22 @@ def _transform_request_body( # noqa: PLR0915
]
data = RequestBody(contents=content)
if system_instructions is not None:
data["system_instruction"] = system_instructions
if tools is not None:
data["tools"] = tools
if tool_choice is not None:
data["toolConfig"] = tool_choice
if include_server_side_tool_invocations:
if "toolConfig" not in data:
data["toolConfig"] = {}
data["toolConfig"]["includeServerSideToolInvocations"] = True
# Vertex rejects system_instruction/tools/toolConfig alongside cachedContent.
# Treat dropping these fields as a request mutation guarded by modify_params.
can_send_cache_incompatible_fields = (
cached_content is None or litellm.modify_params is False
)
if can_send_cache_incompatible_fields:
if system_instructions is not None:
data["system_instruction"] = system_instructions
if tools is not None:
data["tools"] = tools
if tool_choice is not None:
data["toolConfig"] = tool_choice
if include_server_side_tool_invocations:
if "toolConfig" not in data:
data["toolConfig"] = {}
data["toolConfig"]["includeServerSideToolInvocations"] = True
if safety_settings is not None:
data["safetySettings"] = safety_settings
if generation_config is not None and len(generation_config) > 0:

View file

@ -86,6 +86,76 @@ def test_check_if_part_exists_in_parts_camel_case_snake_case():
assert check_if_part_exists_in_parts(parts_mixed, part_mixed_casing)
def test_cached_content_respects_modify_params_for_cache_incompatible_fields():
"""Regression: cachedContent drops system/tools/toolConfig only when modify_params=True."""
import litellm
cache_name = "projects/p/locations/us-central1/cachedContents/abc123"
messages = [
{"role": "system", "content": "You are helpful"},
{"role": "user", "content": "hi"},
]
optional_params = {
"tools": [
{
"functionDeclarations": [
{"name": "get_weather", "description": "Get weather"},
]
}
],
"tool_choice": {"functionCallingConfig": {"mode": "AUTO"}},
}
original_modify_params = litellm.modify_params
try:
# With modify_params=False (default), keep fields even with cachedContent.
litellm.modify_params = False
result = _transform_request_body(
messages=list(messages),
model="gemini-2.5-pro",
optional_params=dict(optional_params),
custom_llm_provider="vertex_ai",
litellm_params={},
cached_content=cache_name,
)
assert result.get("cachedContent") == cache_name
assert "system_instruction" in result
assert "tools" in result
assert "toolConfig" in result
assert "contents" in result
# With modify_params=True, drop cache-incompatible fields.
litellm.modify_params = True
result_modify_true = _transform_request_body(
messages=list(messages),
model="gemini-2.5-pro",
optional_params=dict(optional_params),
custom_llm_provider="vertex_ai",
litellm_params={},
cached_content=cache_name,
)
assert result_modify_true.get("cachedContent") == cache_name
assert "system_instruction" not in result_modify_true
assert "tools" not in result_modify_true
assert "toolConfig" not in result_modify_true
assert "contents" in result_modify_true
# Without cache, fields are always included.
result_no_cache = _transform_request_body(
messages=list(messages),
model="gemini-2.5-pro",
optional_params=dict(optional_params),
custom_llm_provider="vertex_ai",
litellm_params={},
cached_content=None,
)
assert "system_instruction" in result_no_cache
assert "tools" in result_no_cache
assert "toolConfig" in result_no_cache
finally:
litellm.modify_params = original_modify_params
# Tests for issue #14556: Labels field provider-aware filtering
def test_google_genai_excludes_labels():
"""Test that Google GenAI/AI Studio endpoints exclude labels when custom_llm_provider='gemini'"""