From 820d63f3a648637bc897eefc86e9ef76723a5863 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Thu, 12 Mar 2026 16:07:38 -0700 Subject: [PATCH 1/4] =?UTF-8?q?bump:=20version=201.82.1=20=E2=86=92=201.82?= =?UTF-8?q?.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 4a4728fd2f2..840c4706933 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "litellm" -version = "1.82.1" +version = "1.82.2" description = "Library to easily interface with LLM API providers" authors = ["BerriAI"] license = "MIT" @@ -183,7 +183,7 @@ requires = ["poetry-core", "wheel"] build-backend = "poetry.core.masonry.api" [tool.commitizen] -version = "1.82.1" +version = "1.82.2" version_files = [ "pyproject.toml:^version" ] From 92d39c308cd6426943ad7a0a83569cc9647815fb Mon Sep 17 00:00:00 2001 From: Emerson Gomes Date: Thu, 12 Mar 2026 19:48:09 -0500 Subject: [PATCH 2/4] fix(gemini): preserve toolConfig on native generate_content (#23493) --- litellm/google_genai/main.py | 21 ++++++++ .../base_llm/google_genai/transformation.py | 2 + litellm/llms/custom_httpx/llm_http_handler.py | 5 ++ .../gemini/google_genai/transformation.py | 3 ++ .../vertex_ai/google_genai/transformation.py | 6 ++- .../test_google_gemini_proxy_request.py | 3 ++ .../google_genai/test_google_genai_main.py | 53 ++++++++++++------- .../test_google_genai_transformation.py | 26 +++++++++ 8 files changed, 100 insertions(+), 19 deletions(-) diff --git a/litellm/google_genai/main.py b/litellm/google_genai/main.py index a937a35da25..7c97975a54a 100644 --- a/litellm/google_genai/main.py +++ b/litellm/google_genai/main.py @@ -39,6 +39,15 @@ ################################################# +def _get_tool_config_from_kwargs(kwargs: Dict[str, Any]) -> Optional[Dict[str, Any]]: + """Read toolConfig/tool_config without dropping intentionally empty dicts.""" + if "toolConfig" in kwargs: + return kwargs["toolConfig"] + if "tool_config" in kwargs: + return kwargs["tool_config"] + return None + + class GenerateContentSetupResult(BaseModel): """Internal Type - Result of setting up a generate content call""" @@ -171,12 +180,14 @@ def setup_generate_content_call( system_instruction = kwargs.get("systemInstruction") or kwargs.get( "system_instruction" ) + tool_config = _get_tool_config_from_kwargs(kwargs) request_body = ( generate_content_provider_config.transform_generate_content_request( model=model, contents=contents, tools=tools, generate_content_config_dict=generate_content_config_dict, + tool_config=tool_config, system_instruction=system_instruction, ) ) @@ -323,6 +334,7 @@ def generate_content( system_instruction = kwargs.get("systemInstruction") or kwargs.get( "system_instruction" ) + tool_config = _get_tool_config_from_kwargs(kwargs) # Check if we should use the adapter (when provider config is None) if setup_result.generate_content_provider_config is None: @@ -354,6 +366,7 @@ def generate_content( _is_async=_is_async, client=kwargs.get("client"), litellm_metadata=kwargs.get("litellm_metadata", {}), + tool_config=tool_config, system_instruction=system_instruction, ) @@ -414,6 +427,7 @@ async def agenerate_content_stream( system_instruction = kwargs.get("systemInstruction") or kwargs.get( "system_instruction" ) + tool_config = _get_tool_config_from_kwargs(kwargs) # Check if we should use the adapter (when provider config is None) if setup_result.generate_content_provider_config is None: @@ -452,6 +466,7 @@ async def agenerate_content_stream( client=kwargs.get("client"), stream=True, litellm_metadata=kwargs.get("litellm_metadata", {}), + tool_config=tool_config, system_instruction=system_instruction, ) @@ -520,6 +535,10 @@ def generate_content_stream( ) # Call the handler with streaming enabled (sync version) + system_instruction = kwargs.get("systemInstruction") or kwargs.get( + "system_instruction" + ) + tool_config = _get_tool_config_from_kwargs(kwargs) return base_llm_http_handler.generate_content_handler( model=setup_result.model, contents=contents, @@ -536,6 +555,8 @@ def generate_content_stream( client=kwargs.get("client"), stream=True, litellm_metadata=kwargs.get("litellm_metadata", {}), + tool_config=tool_config, + system_instruction=system_instruction, ) except Exception as e: diff --git a/litellm/llms/base_llm/google_genai/transformation.py b/litellm/llms/base_llm/google_genai/transformation.py index e8b3bf1a576..7952e2b0e10 100644 --- a/litellm/llms/base_llm/google_genai/transformation.py +++ b/litellm/llms/base_llm/google_genai/transformation.py @@ -152,6 +152,7 @@ def transform_generate_content_request( contents: GenerateContentContentListUnionDict, tools: Optional[ToolConfigDict], generate_content_config_dict: Dict, + tool_config: Optional[Dict[str, Any]] = None, system_instruction: Optional[Any] = None, ) -> dict: """ @@ -161,6 +162,7 @@ def transform_generate_content_request( model: The model name contents: Input contents tools: Tools + tool_config: Tool configuration generate_content_config_dict: Generation config parameters system_instruction: Optional system instruction diff --git a/litellm/llms/custom_httpx/llm_http_handler.py b/litellm/llms/custom_httpx/llm_http_handler.py index 4e6c3cba684..3e7a636640f 100644 --- a/litellm/llms/custom_httpx/llm_http_handler.py +++ b/litellm/llms/custom_httpx/llm_http_handler.py @@ -9329,6 +9329,7 @@ def generate_content_handler( client: Optional[Union[HTTPHandler, AsyncHTTPHandler]] = None, stream: bool = False, litellm_metadata: Optional[Dict[str, Any]] = None, + tool_config: Optional[Dict[str, Any]] = None, system_instruction: Optional[Any] = None, ) -> Any: """ @@ -9346,6 +9347,7 @@ def generate_content_handler( generate_content_provider_config=generate_content_provider_config, generate_content_config_dict=generate_content_config_dict, tools=tools, + tool_config=tool_config, custom_llm_provider=custom_llm_provider, litellm_params=litellm_params, logging_obj=logging_obj, @@ -9384,6 +9386,7 @@ def generate_content_handler( model=model, contents=contents, tools=tools, + tool_config=tool_config, generate_content_config_dict=generate_content_config_dict, system_instruction=system_instruction, ) @@ -9456,6 +9459,7 @@ async def async_generate_content_handler( client: Optional[AsyncHTTPHandler] = None, stream: bool = False, litellm_metadata: Optional[Dict[str, Any]] = None, + tool_config: Optional[Dict[str, Any]] = None, system_instruction: Optional[Any] = None, ) -> Any: """ @@ -9493,6 +9497,7 @@ async def async_generate_content_handler( model=model, contents=contents, tools=tools, + tool_config=tool_config, generate_content_config_dict=generate_content_config_dict, system_instruction=system_instruction, ) diff --git a/litellm/llms/gemini/google_genai/transformation.py b/litellm/llms/gemini/google_genai/transformation.py index 7c4c7dba626..24f59b8072d 100644 --- a/litellm/llms/gemini/google_genai/transformation.py +++ b/litellm/llms/gemini/google_genai/transformation.py @@ -308,6 +308,7 @@ def transform_generate_content_request( contents: GenerateContentContentListUnionDict, tools: Optional[ToolConfigDict], generate_content_config_dict: Dict, + tool_config: Optional[Dict[str, Any]] = None, system_instruction: Optional[Any] = None, ) -> dict: from litellm.types.google_genai.main import ( @@ -326,6 +327,8 @@ def transform_generate_content_request( if system_instruction is not None: request_dict["systemInstruction"] = system_instruction + if tool_config is not None: + request_dict["toolConfig"] = tool_config return request_dict def transform_generate_content_response( diff --git a/litellm/llms/vertex_ai/google_genai/transformation.py b/litellm/llms/vertex_ai/google_genai/transformation.py index d7a4ceeb3e7..18836b164de 100644 --- a/litellm/llms/vertex_ai/google_genai/transformation.py +++ b/litellm/llms/vertex_ai/google_genai/transformation.py @@ -73,6 +73,7 @@ def transform_generate_content_request( contents: Any, tools: Optional[Any], generate_content_config_dict: Dict, + tool_config: Optional[Dict[str, Any]] = None, system_instruction: Optional[Any] = None, ) -> dict: """ @@ -89,8 +90,11 @@ def transform_generate_content_request( if tools: result["tools"] = tools + if tool_config is not None: + result["toolConfig"] = tool_config + # Add systemInstruction if provided - if system_instruction: + if system_instruction is not None: result["systemInstruction"] = system_instruction # Handle generationConfig - Vertex AI expects it in the same format diff --git a/tests/proxy_unit_tests/test_google_gemini_proxy_request.py b/tests/proxy_unit_tests/test_google_gemini_proxy_request.py index 90c2cac18d0..92b4b9af496 100644 --- a/tests/proxy_unit_tests/test_google_gemini_proxy_request.py +++ b/tests/proxy_unit_tests/test_google_gemini_proxy_request.py @@ -174,6 +174,7 @@ async def test_google_gemini_httpx_request_direct(): ], "role": "user" }, + "toolConfig": {"functionCallingConfig": {"mode": "ANY"}}, "config": { # Note: already transformed from generationConfig "temperature": 0, "topP": 1, @@ -240,6 +241,7 @@ async def test_google_gemini_httpx_request_direct(): generate_content_provider_config=provider_config, generate_content_config_dict=sample_payload["config"], tools=None, + tool_config=sample_payload["toolConfig"], custom_llm_provider="gemini", litellm_params=litellm_params, logging_obj=logging_obj, @@ -265,6 +267,7 @@ async def test_google_gemini_httpx_request_direct(): request_data = call_kwargs.get('json') if request_data: assert 'contents' in request_data, "Expected 'contents' in request data" + assert request_data["toolConfig"] == sample_payload["toolConfig"] # The config should be included in the request as generationConfig if 'generationConfig' in request_data: diff --git a/tests/test_litellm/google_genai/test_google_genai_main.py b/tests/test_litellm/google_genai/test_google_genai_main.py index 5854e4b55af..5eb5c6a1177 100644 --- a/tests/test_litellm/google_genai/test_google_genai_main.py +++ b/tests/test_litellm/google_genai/test_google_genai_main.py @@ -1,24 +1,13 @@ #!/usr/bin/env python3 -""" -Test to verify the Google GenAI generate_content adapter functionality -""" -import json -import os -import sys - -import pytest - -sys.path.insert( - 0, os.path.abspath("../../..") -) # Adds the parent directory to the system path +"""Tests for Google GenAI main entrypoints.""" -import json import os import sys +from unittest.mock import AsyncMock, MagicMock, patch import pytest -import litellm +sys.path.insert(0, os.path.abspath("../../..")) @pytest.mark.asyncio @@ -26,8 +15,6 @@ async def test_agenerate_content_stream(): """ Test that the agenerate_content_stream function works """ - from unittest.mock import AsyncMock, patch - from litellm.google_genai.main import ( agenerate_content_stream, base_llm_http_handler, @@ -36,10 +23,40 @@ async def test_agenerate_content_stream(): with patch.object( base_llm_http_handler, "generate_content_handler", new=AsyncMock() ) as mock_post: - result = await agenerate_content_stream( + await agenerate_content_stream( model="gemini/gemini-2.0-flash-001", contents="Hello, world!", stream=True, ) mock_post.assert_called_once() - mock_post.call_args.kwargs["stream"] == True + assert mock_post.call_args.kwargs["stream"] is True + + +def test_generate_content_stream_forwards_system_instruction(): + """Test that generate_content_stream forwards systemInstruction and toolConfig.""" + from litellm.google_genai.main import ( + base_llm_http_handler, + generate_content_stream, + ) + + mock_response = MagicMock() + tool_config = {"functionCallingConfig": {"mode": "ANY"}} + + with patch.object( + base_llm_http_handler, "generate_content_handler", return_value=mock_response + ) as mock_post: + result = generate_content_stream( + model="gemini/gemini-2.0-flash-001", + contents="Hello, world!", + stream=True, + systemInstruction={"parts": [{"text": "You are helpful"}]}, + toolConfig=tool_config, + ) + + assert result is mock_response + mock_post.assert_called_once() + assert mock_post.call_args.kwargs["stream"] is True + assert mock_post.call_args.kwargs["tool_config"] == tool_config + assert mock_post.call_args.kwargs["system_instruction"] == { + "parts": [{"text": "You are helpful"}] + } diff --git a/tests/test_litellm/google_genai/test_google_genai_transformation.py b/tests/test_litellm/google_genai/test_google_genai_transformation.py index 8943d198dc1..f5f63db819c 100644 --- a/tests/test_litellm/google_genai/test_google_genai_transformation.py +++ b/tests/test_litellm/google_genai/test_google_genai_transformation.py @@ -12,6 +12,9 @@ import pytest from litellm.llms.gemini.google_genai.transformation import GoogleGenAIConfig +from litellm.llms.vertex_ai.google_genai.transformation import ( + VertexAIGoogleGenAIConfig, +) from litellm.responses.litellm_completion_transformation.transformation import ( LiteLLMCompletionResponsesConfig, ) @@ -173,6 +176,26 @@ def test_map_generate_content_optional_params_response_mime_type(): assert "responseJsonSchema" in result +@pytest.mark.parametrize( + "config_cls", + [GoogleGenAIConfig, VertexAIGoogleGenAIConfig], +) +def test_transform_generate_content_request_preserves_tool_config(config_cls): + config = config_cls() + tool_config = {"functionCallingConfig": {"mode": "ANY"}} + + result = config.transform_generate_content_request( + model="gemini-3-flash-preview", + contents=[{"role": "user", "parts": [{"text": "hello"}]}], + tools=[{"functionDeclarations": [{"name": "execute_command"}]}], + tool_config=tool_config, + generate_content_config_dict={"temperature": 1}, + system_instruction={"parts": [{"text": "system"}]}, + ) + + assert result["toolConfig"] == tool_config + + def test_responses_api_reasoning_dict_format(): """Test that reasoning parameter with dict format is mapped to reasoning_effort""" from litellm.types.llms.openai import ResponsesAPIOptionalRequestParams @@ -274,6 +297,7 @@ def test_transform_generate_content_request_with_system_instruction(): model="gemini-3-flash-preview", contents=contents, tools=None, + tool_config=None, generate_content_config_dict=generate_content_config_dict, system_instruction=system_instruction, ) @@ -305,6 +329,7 @@ def test_transform_generate_content_request_without_system_instruction(): model="gemini-3-flash-preview", contents=contents, tools=None, + tool_config=None, generate_content_config_dict=generate_content_config_dict, system_instruction=None, ) @@ -356,6 +381,7 @@ def test_transform_generate_content_request_system_instruction_with_tools(): model="gemini-3-flash-preview", contents=contents, tools=tools, + tool_config=None, generate_content_config_dict=generate_content_config_dict, system_instruction=system_instruction, ) From 9cd7ad263400c0db1ff70b671cbd1717b6582cf5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 13 Mar 2026 05:29:52 +0000 Subject: [PATCH 3/4] chore: regenerate poetry.lock to match pyproject.toml (#23514) Co-authored-by: github-actions[bot] --- poetry.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/poetry.lock b/poetry.lock index 23f4fad175f..bd90b3e2670 100644 --- a/poetry.lock +++ b/poetry.lock @@ -3222,15 +3222,15 @@ files = [ [[package]] name = "litellm-proxy-extras" -version = "0.4.54" +version = "0.4.56" description = "Additional files for the LiteLLM Proxy. Reduces the size of the main litellm package." optional = true python-versions = "!=2.7.*,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,!=3.7.*,>=3.8" groups = ["main"] markers = "extra == \"proxy\"" files = [ - {file = "litellm_proxy_extras-0.4.54-py3-none-any.whl", hash = "sha256:6621cf529f7f3647eb2dd0d2c417d91db8c7a05c3c592bef251887a122928837"}, - {file = "litellm_proxy_extras-0.4.54.tar.gz", hash = "sha256:2c777ecdf39901c4007ade4466eb6398985ed4000afe3fc2cac997e1169e8cee"}, + {file = "litellm_proxy_extras-0.4.56-py3-none-any.whl", hash = "sha256:52dbe3b5358c790e77e12f1ec5ef8e7508b383c2aaf41299750b6fb400908ee7"}, + {file = "litellm_proxy_extras-0.4.56.tar.gz", hash = "sha256:63ad59baa0defccc5c929cfd933ee7e32a6614b0fc5fa0fc45a12d7608e33f08"}, ] [[package]] @@ -8002,4 +8002,4 @@ utils = ["numpydoc"] [metadata] lock-version = "2.1" python-versions = ">=3.9,<4.0" -content-hash = "5ed0af4e3644bc7b5a02b8bfc8b3eda15c014b43aa6da7a9a97a9b070fba5366" +content-hash = "1ade5dee030fd878c907a20b88a6a52ca26ee4ecbed15ecb045f9ae1d4b8b714" From d7c6b95d6f5d8f8440a0bb7ae19c89b798d3e2f3 Mon Sep 17 00:00:00 2001 From: Prafulla Anurag Date: Fri, 13 Mar 2026 15:52:03 +0530 Subject: [PATCH 4/4] fix(docs): correct Docker image tag in v1.82.0 release notes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add missing 'v' prefix to Docker image tag: main-1.82.0-stable → main-v1.82.0-stable Co-Authored-By: Claude Sonnet 4.6 --- docs/my-website/release_notes/v1.82.0.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/my-website/release_notes/v1.82.0.md b/docs/my-website/release_notes/v1.82.0.md index 09967d5889b..6f1daddf979 100644 --- a/docs/my-website/release_notes/v1.82.0.md +++ b/docs/my-website/release_notes/v1.82.0.md @@ -26,7 +26,7 @@ import TabItem from '@theme/TabItem'; docker run \ -e STORE_MODEL_IN_DB=True \ -p 4000:4000 \ -ghcr.io/berriai/litellm:main-1.82.0-stable +ghcr.io/berriai/litellm:main-v1.82.0-stable ```