Skip to content

Commit 11a417c

Browse files
GWealepandego
authored andcommitted
fix: Expand LiteLLM reasoning extraction to include 'reasoning' field
The `_extract_reasoning_value` function now checks for both 'reasoning_content' and 'reasoning' fields in LiteLLM messages, with 'reasoning_content' taking precedence Close google#3694 Co-authored-by: George Weale <gweale@google.com> PiperOrigin-RevId: 878668213
1 parent 2eda12a commit 11a417c

2 files changed

Lines changed: 145 additions & 2 deletions

File tree

src/google/adk/models/lite_llm.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -428,10 +428,18 @@ def _convert_reasoning_value_to_parts(reasoning_value: Any) -> List[types.Part]:
428428

429429

430430
def _extract_reasoning_value(message: Message | Delta | None) -> Any:
431-
"""Fetches the reasoning payload from a LiteLLM message."""
431+
"""Fetches the reasoning payload from a LiteLLM message.
432+
433+
Checks for both 'reasoning_content' (LiteLLM standard, used by Azure/Foundry,
434+
Ollama via LiteLLM) and 'reasoning' (used by LM Studio, vLLM).
435+
Prioritizes 'reasoning_content' when both are present.
436+
"""
432437
if message is None:
433438
return None
434-
return message.get("reasoning_content")
439+
reasoning_content = message.get("reasoning_content")
440+
if reasoning_content is not None:
441+
return reasoning_content
442+
return message.get("reasoning")
435443

436444

437445
class ChatCompletionFileUrlObject(TypedDict, total=False):
@@ -1345,6 +1353,7 @@ def _has_meaningful_signal(message: Message | Delta | None) -> bool:
13451353
or message.get("tool_calls")
13461354
or message.get("function_call")
13471355
or message.get("reasoning_content")
1356+
or message.get("reasoning")
13481357
)
13491358

13501359
if isinstance(response, ModelResponseStream):

tests/unittests/models/test_litellm.py

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
from google.adk.models.lite_llm import _append_fallback_user_content_if_missing
2929
from google.adk.models.lite_llm import _content_to_message_param
3030
from google.adk.models.lite_llm import _enforce_strict_openai_schema
31+
from google.adk.models.lite_llm import _extract_reasoning_value
3132
from google.adk.models.lite_llm import _FILE_ID_REQUIRED_PROVIDERS
3233
from google.adk.models.lite_llm import _FINISH_REASON_MAPPING
3334
from google.adk.models.lite_llm import _function_declaration_to_tool_param
@@ -2338,6 +2339,139 @@ def test_model_response_to_generate_content_response_reasoning_content():
23382339
assert response.content.parts[1].text == "Answer"
23392340

23402341

2342+
def test_message_to_generate_content_response_reasoning_field():
2343+
"""Test that the 'reasoning' field is supported (LM Studio, vLLM)."""
2344+
message = {
2345+
"role": "assistant",
2346+
"content": "Final answer",
2347+
"reasoning": "Thinking process",
2348+
}
2349+
response = _message_to_generate_content_response(message)
2350+
2351+
assert len(response.content.parts) == 2
2352+
thought_part = response.content.parts[0]
2353+
text_part = response.content.parts[1]
2354+
assert thought_part.text == "Thinking process"
2355+
assert thought_part.thought is True
2356+
assert text_part.text == "Final answer"
2357+
2358+
2359+
def test_model_response_to_generate_content_response_reasoning_field():
2360+
"""Test that 'reasoning' field is supported in ModelResponse."""
2361+
model_response = ModelResponse(
2362+
model="test-model",
2363+
choices=[{
2364+
"message": {
2365+
"role": "assistant",
2366+
"content": "Result",
2367+
"reasoning": "Chain of thought",
2368+
},
2369+
"finish_reason": "stop",
2370+
}],
2371+
)
2372+
2373+
response = _model_response_to_generate_content_response(model_response)
2374+
2375+
assert response.content.parts[0].text == "Chain of thought"
2376+
assert response.content.parts[0].thought is True
2377+
assert response.content.parts[1].text == "Result"
2378+
2379+
2380+
def test_reasoning_content_takes_precedence_over_reasoning():
2381+
"""Test that 'reasoning_content' is prioritized over 'reasoning'."""
2382+
message = {
2383+
"role": "assistant",
2384+
"content": "Answer",
2385+
"reasoning_content": "LiteLLM standard reasoning",
2386+
"reasoning": "Alternative reasoning",
2387+
}
2388+
response = _message_to_generate_content_response(message)
2389+
2390+
assert len(response.content.parts) == 2
2391+
thought_part = response.content.parts[0]
2392+
assert thought_part.text == "LiteLLM standard reasoning"
2393+
assert thought_part.thought is True
2394+
2395+
2396+
def test_extract_reasoning_value_from_reasoning_content():
2397+
"""Test extraction from reasoning_content (LiteLLM standard)."""
2398+
message = ChatCompletionAssistantMessage(
2399+
role="assistant",
2400+
content="Answer",
2401+
reasoning_content="LiteLLM reasoning",
2402+
)
2403+
result = _extract_reasoning_value(message)
2404+
assert result == "LiteLLM reasoning"
2405+
2406+
2407+
def test_extract_reasoning_value_from_reasoning():
2408+
"""Test extraction from reasoning (LM Studio, vLLM)."""
2409+
2410+
class MockMessage:
2411+
2412+
def __init__(self):
2413+
self.role = "assistant"
2414+
self.content = "Answer"
2415+
self.reasoning = "Alternative reasoning"
2416+
2417+
def get(self, key, default=None):
2418+
return getattr(self, key, default)
2419+
2420+
message = MockMessage()
2421+
result = _extract_reasoning_value(message)
2422+
assert result == "Alternative reasoning"
2423+
2424+
2425+
def test_extract_reasoning_value_dict_reasoning_content():
2426+
"""Test extraction from dict with reasoning_content field."""
2427+
message = {
2428+
"role": "assistant",
2429+
"content": "Answer",
2430+
"reasoning_content": "Dict reasoning content",
2431+
}
2432+
result = _extract_reasoning_value(message)
2433+
assert result == "Dict reasoning content"
2434+
2435+
2436+
def test_extract_reasoning_value_dict_reasoning():
2437+
"""Test extraction from dict with reasoning field."""
2438+
message = {
2439+
"role": "assistant",
2440+
"content": "Answer",
2441+
"reasoning": "Dict reasoning",
2442+
}
2443+
result = _extract_reasoning_value(message)
2444+
assert result == "Dict reasoning"
2445+
2446+
2447+
def test_extract_reasoning_value_dict_prefers_reasoning_content():
2448+
"""Test that reasoning_content takes precedence over reasoning in dicts."""
2449+
message = {
2450+
"role": "assistant",
2451+
"content": "Answer",
2452+
"reasoning_content": "Primary",
2453+
"reasoning": "Secondary",
2454+
}
2455+
result = _extract_reasoning_value(message)
2456+
assert result == "Primary"
2457+
2458+
2459+
def test_extract_reasoning_value_none_message():
2460+
"""Test that None message returns None."""
2461+
result = _extract_reasoning_value(None)
2462+
assert result is None
2463+
2464+
2465+
def test_extract_reasoning_value_no_reasoning_fields():
2466+
"""Test that None is returned when no reasoning fields exist."""
2467+
message = {
2468+
"role": "assistant",
2469+
"content": "Answer only",
2470+
}
2471+
result = _extract_reasoning_value(message)
2472+
assert result is None
2473+
2474+
23412475
def test_parse_tool_calls_from_text_multiple_calls():
23422476
text = (
23432477
'{"name":"alpha","arguments":{"value":1}}\n'

0 commit comments

Comments
 (0)