Repository: google/adk-python

Issue:
LLM Generation Config(generate_content_config) does not work.
** Please make sure you read the contribution guide and file the issues in the right place. **
[Contribution guide.](https://google.github.io/adk-docs/contributing-guide/)

**Describe the bug**
A clear and concise description of what the bug is.

**To Reproduce**
Steps to reproduce the behavior:

> [!NOTE]
> ```bash
> LLM server way: vLLM
> Model Info: Qwen2.5-14B-Instruct
> ```

### :x: Case1: Custom `temperature=0.2` with `generate_content_config`

```diff
import datetime
from zoneinfo import ZoneInfo

from google.adk.agents import Agent
from google.adk.models.lite_llm import LiteLlm
from google.genai import types


def get_weather(city: str) -> dict:
    """Retrieves the current weather report for a specified city.

    Args:
        city (str): The name of the city for which to retrieve the weather report.

    Returns:
        dict: status and result or error msg.
    """
    if city.lower() == "new york":
        return {
            "status": "success",
            "report": (
                "The weather in New York is sunny with a temperature of 25 degrees"
                " Celsius (77 degrees Fahrenheit)."
            ),
        }
    else:
        return {
            "status": "error",
            "error_message": f"Weather information for '{city}' is not available.",
        }


def get_current_time(city: str) -> dict:
    """Returns the current time in a specified city.

    Args:
        city (str): The name of the city for which to retrieve the current time.

    Returns:
        dict: status and result or error msg.
    """

    if city.lower() == "new york":
        tz_identifier = "America/New_York"
    else:
        return {
            "status": "error",
            "error_message": (
                f"Sorry, I don't have timezone information for {city}."
            ),
        }

    tz = ZoneInfo(tz_identifier)
    now = datetime.datetime.now(tz)
    report = (
        f'The current time in {city} is {now.strftime("%Y-%m-%d %H:%M:%S %Z%z")}'
    )
    return {"status": "success", "report": report}


root_agent = Agent(
    model=LiteLlm(
        model="openai/Qwen2.5-14B-Instruct",
        api_base = "http://127.0.0.1:8080/v1",
        api_key="EMPTY",
    ),
    name="weather_time_agent",
    description=(
        "Agent to answer questions about the time and weather in a city."
    ),
    instruction=(
        "You are a helpful agent who can answer user questions about the time and weather in a city."
    ),
    tools=[get_weather, get_current_time],
+   generate_content_config=types.GenerateContentConfig(
+      temperature=0.2, # More deterministic output
+      max_output_tokens=250
    )
)
```

```bash
adk web
```

- Input: `Hello`
- Output: `Hello! How can I assist you today? Do you want to know the time or weather in a specific city?`

<img width="1507" alt="Image" src="https://github.com/user-attachments/assets/14d92bf3-5107-4dd2-ad9c-af629e7e8c62" />

> [!IMPORTANT]
>  LLM server use default `temperature=0.7`, while `generate_content_config` does not work (both temperature and max_output_tokens.

---

### :white_check_mark: Case2: Custom `temperature=0.2` with `generate_content_config` and LiteLLM args `temperature=0.1`

```diff
import datetime
from zoneinfo import ZoneInfo

from google.adk.agents import Agent
from google.adk.models.lite_llm import LiteLlm
from google.genai import types


def get_weather(city: str) -> dict:
    """Retrieves the current weather report for a specified city.

    Args:
        city (str): The name of the city for which to retrieve the weather report.

    Returns:
        dict: status and result or error msg.
    """
    if city.lower() == "new york":
        return {
            "status": "success",
            "report": (
                "The weather in New York is sunny with a temperature of 25 degrees"
                " Celsius (77 degrees Fahrenheit)."
            ),
        }
    else:
        return {
            "status": "error",
            "error_message": f"Weather information for '{city}' is not available.",
        }


def get_current_time(city: str) -> dict:
    """Returns the current time in a specified city.

    Args:
        city (str): The name of the city for which to retrieve the current time.

    Returns:
        dict: status and result or error msg.
    """

    if city.lower() == "new york":
        tz_identifier = "America/New_York"
    else:
        return {
            "status": "error",
            "error_message": (
                f"Sorry, I don't have timezone information for {city}."
            ),
        }

    tz = ZoneInfo(tz_identifier)
    now = datetime.datetime.now(tz)
    report = (
        f'The current time in {city} is {now.strftime("%Y-%m-%d %H:%M:%S %Z%z")}'
    )
    return {"status": "success", "report": report}


root_agent = Agent(
    model=LiteLlm(
        model="openai/Qwen2.5-14B-Instruct",
        api_base = "http://127.0.0.1:8080/v1",
        api_key="EMPTY",
+       temperature=0.1,
    ),
    name="weather_time_agent",
    description=(
        "Agent to answer questions about the time and weather in a city."
    ),
    instruction=(
        "You are a helpful agent who can answer user questions about the time and weather in a city."
    ),
    tools=[get_weather, get_current_time],
+   generate_content_config=types.GenerateContentConfig(
+      temperature=0.2, # More deterministic output
+      max_output_tokens=250
    )
)
```

```bash
adk web
```

- Input: `Hello`
- Output: `Hello! How can I assist you today? Do you want to know the time or weather in a specific city?`

<img width="1507" alt="Image" src="https://github.com/user-attachments/assets/209baf28-39d0-417e-aeff-14e0c453f75e" />

> [!IMPORTANT]
>  LLM server use `temperature=0.1`, which is same as LiteLLM args. On the other hand, `generate_content_config` also does not work (both temperature and max_output_tokens.

**Expected behavior**
A clear and concise description of what you expected to happen.

**Screenshots**
If applicable, add screenshots to help explain your problem.

**Desktop (please complete the following information):**
 - OS: `macOS`
 - Python version(python -V): `python 3.12`
 - ADK version(pip show google-adk): `1.2.1`

**Additional context**
Add any other context about the problem here.



Discussion:
Root code maybe  `src/google/adk/models/lite_llm.py`, func `generate_content_async `.

Parameter `completion_args` not contains generate config like `top_k`, `temperature`, etc, while func args`llm_request` contains `generate_content_config` which parameter we expected.

https://github.com/google/adk-python/blob/068df04bcef694725dd36e09f4476b5e67f1b456/src/google/adk/models/lite_llm.py#L647-L771
I will fix this issue as possible as I can.



Patch:
diff --git a/src/google/adk/models/lite_llm.py b/src/google/adk/models/lite_llm.py
index ed54faecf..c954711ad 100644
--- a/src/google/adk/models/lite_llm.py
+++ b/src/google/adk/models/lite_llm.py
@@ -23,6 +23,7 @@
 from typing import Dict
 from typing import Generator
 from typing import Iterable
+from typing import List
 from typing import Literal
 from typing import Optional
 from typing import Tuple
@@ -481,16 +482,22 @@ def _message_to_generate_content_response(
 
 def _get_completion_inputs(
     llm_request: LlmRequest,
-) -> tuple[Iterable[Message], Iterable[dict]]:
-  """Converts an LlmRequest to litellm inputs.
+) -> Tuple[
+    List[Message],
+    Optional[List[Dict]],
+    Optional[types.SchemaUnion],
+    Optional[Dict],
+]:
+  """Converts an LlmRequest to litellm inputs and extracts generation params.
 
   Args:
     llm_request: The LlmRequest to convert.
 
   Returns:
-    The litellm inputs (message list, tool dictionary and response format).
+    The litellm inputs (message list, tool dictionary, response format, and generation params).
   """
-  messages = []
+  # 1. Construct messages
+  messages: List[Message] = []
   for content in llm_request.contents or []:
     message_param_or_list = _content_to_message_param(content)
     if isinstance(message_param_or_list, list):
@@ -507,7 +514,8 @@ def _get_completion_inputs(
         ),
     )
 
-  tools = None
+  # 2. Convert tool declarations
+  tools: Optional[List[Dict]] = None
   if (
       llm_request.config
       and llm_request.config.tools
@@ -518,12 +526,39 @@ def _get_completion_inputs(
         for tool in llm_request.config.tools[0].function_declarations
     ]
 
-  response_format = None
+  # 3. Handle response format
+  response_format: Optional[types.SchemaUnion] = (
+      llm_request.config.response_schema if llm_request.config else None
+  )
+
+  # 4. Extract generation parameters
+  generation_params: Optional[Dict] = None
+  if llm_request.config:
+    config_dict = llm_request.config.model_dump(exclude_none=True)
+    # Generate LiteLlm parameters here,
+    # Following https://docs.litellm.ai/docs/completion/input.
+    generation_params = {}
+    param_mapping = {
+        "max_output_tokens": "max_completion_tokens",
+        "stop_sequences": "stop",
+    }
+    for key in (
+        "temperature",
+        "max_output_tokens",
+        "top_p",
+        "top_k",
+        "stop_sequences",
+        "presence_penalty",
+        "frequency_penalty",
+    ):
+      if key in config_dict:
+        mapped_key = param_mapping.get(key, key)
+        generation_params[mapped_key] = config_dict[key]
 
-  if llm_request.config.response_schema:
-    response_format = llm_request.config.response_schema
+      if not generation_params:
+        generation_params = None
 
-  return messages, tools, response_format
+  return messages, tools, response_format, generation_params
 
 
 def _build_function_declaration_log(
@@ -660,7 +695,9 @@ async def generate_content_async(
     self._maybe_append_user_content(llm_request)
     logger.debug(_build_request_log(llm_request))
 
-    messages, tools, response_format = _get_completion_inputs(llm_request)
+    messages, tools, response_format, generation_params = (
+        _get_completion_inputs(llm_request)
+    )
 
     completion_args = {
         "model": self.model,
@@ -668,7 +705,13 @@ async def generate_content_async(
         "tools": tools,
         "response_format": response_format,
     }
-    completion_args.update(self._additional_args)
+
+    # Merge additional arguments and generation parameters safely
+    if hasattr(self, "_additional_args") and self._additional_args:
+      completion_args.update(self._additional_args)
+
+    if generation_params:
+      completion_args.update(generation_params)
 
     if stream:
       text = ""


Test patch:
diff --git a/tests/unittests/models/test_litellm.py b/tests/unittests/models/test_litellm.py
index f316e83ae..e600ee7f0 100644
--- a/tests/unittests/models/test_litellm.py
+++ b/tests/unittests/models/test_litellm.py
@@ -13,7 +13,6 @@
 # limitations under the License.
 
 
-import json
 from unittest.mock import AsyncMock
 from unittest.mock import Mock
 
@@ -1430,3 +1429,35 @@ async def test_generate_content_async_non_compliant_multiple_function_calls(
   assert final_response.content.parts[1].function_call.name == "function_2"
   assert final_response.content.parts[1].function_call.id == "1"
   assert final_response.content.parts[1].function_call.args == {"arg": "value2"}
+
+
+@pytest.mark.asyncio
+def test_get_completion_inputs_generation_params():
+  # Test that generation_params are extracted and mapped correctly
+  req = LlmRequest(
+      contents=[
+          types.Content(role="user", parts=[types.Part.from_text(text="hi")]),
+      ],
+      config=types.GenerateContentConfig(
+          temperature=0.33,
+          max_output_tokens=123,
+          top_p=0.88,
+          top_k=7,
+          stop_sequences=["foo", "bar"],
+          presence_penalty=0.1,
+          frequency_penalty=0.2,
+      ),
+  )
+  from google.adk.models.lite_llm import _get_completion_inputs
+
+  _, _, _, generation_params = _get_completion_inputs(req)
+  assert generation_params["temperature"] == 0.33
+  assert generation_params["max_completion_tokens"] == 123
+  assert generation_params["top_p"] == 0.88
+  assert generation_params["top_k"] == 7
+  assert generation_params["stop"] == ["foo", "bar"]
+  assert generation_params["presence_penalty"] == 0.1
+  assert generation_params["frequency_penalty"] == 0.2
+  # Should not include max_output_tokens
+  assert "max_output_tokens" not in generation_params
+  assert "stop_sequences" not in generation_params