diff --git a/litellm/llms/groq/chat/transformation.py b/litellm/llms/groq/chat/transformation.py index a75ecd8cc7..34ea7b03dd 100644 --- a/litellm/llms/groq/chat/transformation.py +++ b/litellm/llms/groq/chat/transformation.py @@ -323,4 +323,12 @@ class GroqChatCompletionStreamingHandler(OpenAIChatCompletionStreamingHandler): status_code=error.get("code"), message=error.get("message"), body=error ) + # Map Groq's 'reasoning' field to LiteLLM's 'reasoning_content' field + # Groq returns delta.reasoning, but LiteLLM expects delta.reasoning_content + choices = chunk.get("choices", []) + for choice in choices: + delta = choice.get("delta", {}) + if "reasoning" in delta: + delta["reasoning_content"] = delta.pop("reasoning") + return super().chunk_parser(chunk) diff --git a/tests/llm_translation/test_groq.py b/tests/llm_translation/test_groq.py index 33c1a42587..b82cffd418 100644 --- a/tests/llm_translation/test_groq.py +++ b/tests/llm_translation/test_groq.py @@ -11,7 +11,10 @@ import pytest import litellm from base_llm_unit_tests import BaseLLMChatTest -from litellm.llms.groq.chat.transformation import GroqChatConfig +from litellm.llms.groq.chat.transformation import ( + GroqChatConfig, + GroqChatCompletionStreamingHandler, +) class TestGroq(BaseLLMChatTest): def get_base_completion_call_args(self) -> dict: @@ -164,3 +167,124 @@ class TestGroqStructuredOutputs: if "tools" in result: tool_names = [t.get("function", {}).get("name") for t in result["tools"]] assert "json_tool_call" not in tool_names + + +class TestGroqReasoning: + """ + Tests for Groq reasoning field mapping. + + Groq returns 'reasoning' field in delta, but LiteLLM expects 'reasoning_content'. + """ + + def test_reasoning_field_mapping_in_streaming_chunks(self): + """ + Test that Groq's 'reasoning' field in streaming chunks is properly mapped + to LiteLLM's 'reasoning_content' field. + """ + handler = GroqChatCompletionStreamingHandler( + streaming_response=None, sync_stream=True + ) + + # Simulate a chunk with reasoning field as returned by Groq + groq_chunk = { + "id": "chatcmpl-test", + "object": "chat.completion.chunk", + "created": 1769511767, + "model": "qwen/qwen3-32b", + "choices": [ + { + "delta": { + "reasoning": "This is reasoning content", + "role": None, + }, + "finish_reason": None, + "index": 0, + } + ], + } + + # Parse the chunk + parsed_chunk = handler.chunk_parser(groq_chunk) + + # Verify that reasoning was mapped to reasoning_content + assert parsed_chunk.choices[0].delta.reasoning_content == "This is reasoning content" + # Verify that the original 'reasoning' field was removed + assert not hasattr(parsed_chunk.choices[0].delta, "reasoning") + + def test_reasoning_field_not_present(self): + """ + Test that chunks without reasoning field still work correctly. + """ + handler = GroqChatCompletionStreamingHandler( + streaming_response=None, sync_stream=True + ) + + # Simulate a chunk without reasoning field + groq_chunk = { + "id": "chatcmpl-test", + "object": "chat.completion.chunk", + "created": 1769511767, + "model": "qwen/qwen3-32b", + "choices": [ + { + "delta": { + "content": "Regular content", + "role": "assistant", + }, + "finish_reason": None, + "index": 0, + } + ], + } + + # Parse the chunk + parsed_chunk = handler.chunk_parser(groq_chunk) + + # Verify that content is present + assert parsed_chunk.choices[0].delta.content == "Regular content" + assert parsed_chunk.choices[0].delta.role == "assistant" + # Verify that reasoning_content is not set (it should be deleted by Delta.__init__) + assert not hasattr(parsed_chunk.choices[0].delta, "reasoning_content") + + def test_reasoning_with_tool_calls(self): + """ + Test that reasoning field is properly mapped even when tool_calls are present. + """ + handler = GroqChatCompletionStreamingHandler( + streaming_response=None, sync_stream=True + ) + + # Simulate a chunk with both reasoning and tool_calls + groq_chunk = { + "id": "chatcmpl-test", + "object": "chat.completion.chunk", + "created": 1769511767, + "model": "qwen/qwen3-32b", + "choices": [ + { + "delta": { + "reasoning": "Reasoning before tool call", + "tool_calls": [ + { + "index": 0, + "id": "call_123", + "function": {"name": "test_function", "arguments": "{}"}, + "type": "function", + } + ], + }, + "finish_reason": None, + "index": 0, + } + ], + } + + # Parse the chunk + parsed_chunk = handler.chunk_parser(groq_chunk) + + # Verify that reasoning was mapped to reasoning_content + assert parsed_chunk.choices[0].delta.reasoning_content == "Reasoning before tool call" + # Verify tool_calls are still present + assert parsed_chunk.choices[0].delta.tool_calls is not None + assert len(parsed_chunk.choices[0].delta.tool_calls) == 1 + assert parsed_chunk.choices[0].delta.tool_calls[0]["function"]["name"] == "test_function"