Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions litellm/llms/vertex_ai/gemini/transformation.py
Original file line number Diff line number Diff line change
Expand Up @@ -529,12 +529,18 @@ def _gemini_convert_messages_with_history( # noqa: PLR0915
raise e


# Keys that LiteLLM consumes internally and must never be forwarded to the

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Truncated comment

The comment on line 532 is cut off mid-sentence — it currently reads # Keys that LiteLLM consumes internally and must never be forwarded to the with no completion. This should be finished to explain what the keys must not be forwarded to.

Suggested change
# Keys that LiteLLM consumes internally and must never be forwarded to the
# Keys that LiteLLM consumes internally and must never be forwarded to the Vertex AI request body.

_LITELLM_INTERNAL_EXTRA_BODY_KEYS: frozenset = frozenset({"cache", "tags"})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hardcoded internal-key list is Vertex AI-only and incomplete

_LITELLM_INTERNAL_EXTRA_BODY_KEYS is defined and consumed only inside the Vertex AI/Gemini transformation. Any other provider that merges extra_body (e.g. future providers added via _pop_and_merge_extra_body or similar helpers) will not benefit from this filter and will silently forward cache/tags to the upstream API, causing the same 400 errors this PR is trying to fix.

Additionally, LiteLLM's proxy layer reads other internal keys from extra_body (e.g. metadata, litellm_metadata) that are not currently included in this frozenset. If those keys are ever added to a user's extra_body, they would still be forwarded to Vertex AI.

A more robust approach would be to centralise this filtering at the point where extra_body is first processed (e.g. in litellm_pre_call_utils.py or in the base provider transformation), so all providers are protected. At the very least, consider whether metadata and litellm_metadata should also be included here.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Type annotation on frozenset is redundant

The type annotation frozenset on the right-hand side literal is redundant since Python infers the type automatically from the frozenset(...) constructor call. More importantly, using the bare frozenset without a subscript loses the element type. Either omit the annotation or make it explicit:

Suggested change
_LITELLM_INTERNAL_EXTRA_BODY_KEYS: frozenset = frozenset({"cache", "tags"})
_LITELLM_INTERNAL_EXTRA_BODY_KEYS: frozenset[str] = frozenset({"cache", "tags"})

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!



def _pop_and_merge_extra_body(data: RequestBody, optional_params: dict) -> None:
"""Pop extra_body from optional_params and shallow-merge into data, deep-merging dict values."""
extra_body: Optional[dict] = optional_params.pop("extra_body", None)
if extra_body is not None:
data_dict: dict = data # type: ignore[assignment]
for k, v in extra_body.items():
if k in _LITELLM_INTERNAL_EXTRA_BODY_KEYS:
continue
if k in data_dict and isinstance(data_dict[k], dict) and isinstance(v, dict):
data_dict[k].update(v)
else:
Expand Down
36 changes: 36 additions & 0 deletions litellm/model_prices_and_context_window_backup.json
Original file line number Diff line number Diff line change
Expand Up @@ -16799,6 +16799,42 @@
"supports_vision": true,
"supports_web_search": true
},
"gemini/gemini-3.1-flash-image-preview": {
"input_cost_per_token": 2.5e-07,
"input_cost_per_token_batches": 1.25e-07,
"litellm_provider": "gemini",
"max_input_tokens": 65536,
"max_output_tokens": 32768,
"max_tokens": 32768,
"mode": "image_generation",
"output_cost_per_image": 0.045,
"output_cost_per_image_token": 6e-05,
"output_cost_per_image_token_batches": 3e-05,
"output_cost_per_token": 1.5e-06,
"output_cost_per_token_batches": 7.5e-07,
"rpm": 1000,
"tpm": 4000000,
"source": "https://ai.google.dev/gemini-api/docs/pricing#gemini-3.1-flash-image-preview",
"supported_endpoints": [
"/v1/chat/completions",
"/v1/completions",
"/v1/batch"
],
"supported_modalities": [
"text",
"image"
],
"supported_output_modalities": [
"text",
"image"
],
"supports_function_calling": false,
"supports_prompt_caching": true,
"supports_response_schema": true,
"supports_system_messages": true,
"supports_vision": true,
"supports_web_search": true
},
"gemini/deep-research-pro-preview-12-2025": {
"input_cost_per_image": 0.0011,
"input_cost_per_token": 2e-06,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,75 @@ def test_vertex_ai_includes_labels():



def test_extra_body_cache_not_forwarded_to_vertex_ai():
"""
'cache' inside extra_body is a LiteLLM-internal proxy caching control.
It must NOT be forwarded to the Vertex AI request body.

Regression test for: "Invalid JSON payload received. Unknown name \"cache\": Cannot find field."
Vertex AI enforces a strict JSON schema and rejects any unknown field.
"""
messages = [{"role": "user", "content": "test"}]
optional_params = {
"extra_body": {
"cache": {"use-cache": True, "ttl": 86400}, # LiteLLM-internal
"some_vertex_param": "value", # legitimate provider extra
},
}
litellm_params = {}

result = _transform_request_body(
messages=messages,
model="gemini-2.5-pro",
optional_params=optional_params,
custom_llm_provider="vertex_ai",
litellm_params=litellm_params,
cached_content=None,
)

# 'cache' must be stripped — Vertex AI has no such field
assert "cache" not in result, (
"extra_body.cache must not be forwarded to Vertex AI. "
"Vertex AI rejects it with 400: Unknown name \"cache\": Cannot find field."
)

# Other legitimate extra_body keys should still pass through
assert "some_vertex_param" in result
assert result["some_vertex_param"] == "value"

# Core request fields must be present
assert "contents" in result


def test_extra_body_tags_not_forwarded_to_vertex_ai():
"""
'tags' inside extra_body is a LiteLLM-internal param for logging/tracking.
It must NOT be forwarded to the Vertex AI request body.
Documented in litellm_proxy.md: "Send tags by including them in the extra_body parameter"
"""
messages = [{"role": "user", "content": "test"}]
optional_params = {
"extra_body": {
"tags": ["user:alice", "env:prod"],
"custom_param": "allowed",
},
}
litellm_params = {}

result = _transform_request_body(
messages=messages,
model="gemini-2.5-pro",
optional_params=optional_params,
custom_llm_provider="vertex_ai",
litellm_params=litellm_params,
cached_content=None,
)

assert "tags" not in result
assert "custom_param" in result
assert result["custom_param"] == "allowed"


def test_metadata_to_labels_vertex_only():
"""Test that metadata->labels conversion only happens for Vertex AI"""
messages = [{"role": "user", "content": "test"}]
Expand Down
Loading