291 lines
10 KiB
Python
291 lines
10 KiB
Python
import os
|
|
import sys
|
|
|
|
|
|
import pytest
|
|
|
|
# sys.path.insert(
|
|
# 0, os.path.abspath("../..")
|
|
# ) # noqa
|
|
# ) # Adds the parent directory to the system path
|
|
|
|
import litellm
|
|
from base_llm_unit_tests import BaseLLMChatTest
|
|
from litellm.llms.groq.chat.transformation import (
|
|
GroqChatConfig,
|
|
GroqChatCompletionStreamingHandler,
|
|
)
|
|
|
|
class TestGroq(BaseLLMChatTest):
|
|
def get_base_completion_call_args(self) -> dict:
|
|
return {
|
|
"model": "groq/llama-3.3-70b-versatile",
|
|
}
|
|
|
|
def test_tool_call_no_arguments(self, tool_call_no_arguments):
|
|
"""Test that tool calls with no arguments is translated correctly. Relevant issue: https://github.com/BerriAI/litellm/issues/6833"""
|
|
pass
|
|
|
|
def test_tool_call_with_empty_enum_property(self):
|
|
pass
|
|
|
|
@pytest.mark.parametrize("model", ["groq/qwen/qwen3-32b", "groq/openai/gpt-oss-20b", "groq/openai/gpt-oss-120b"])
|
|
def test_reasoning_effort_in_supported_params(self, model):
|
|
"""Test that reasoning_effort is in the list of supported parameters for Groq"""
|
|
supported_params = GroqChatConfig().get_supported_openai_params(model=model)
|
|
assert "reasoning_effort" in supported_params
|
|
|
|
|
|
class TestGroqStructuredOutputs:
|
|
"""
|
|
Tests for Groq structured outputs handling.
|
|
Related issues:
|
|
- https://github.com/BerriAI/litellm/issues/11001
|
|
- https://github.com/openai/openai-agents-python/issues/2140
|
|
"""
|
|
|
|
def test_structured_output_with_tools_raises_error_for_non_native_models(self):
|
|
"""
|
|
Test that using structured outputs + tools with models that don't support
|
|
native json_schema raises a clear error message.
|
|
|
|
Groq does not support structured outputs + tools together.
|
|
See: https://console.groq.com/docs/structured-outputs
|
|
"Streaming and tool use are not currently supported with Structured Outputs"
|
|
"""
|
|
config = GroqChatConfig()
|
|
|
|
# Model that doesn't support native json_schema
|
|
model = "llama-3.3-70b-versatile"
|
|
|
|
non_default_params = {
|
|
"response_format": {
|
|
"type": "json_schema",
|
|
"json_schema": {
|
|
"name": "test",
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {"name": {"type": "string"}},
|
|
"required": ["name"]
|
|
}
|
|
}
|
|
},
|
|
"tools": [
|
|
{
|
|
"type": "function",
|
|
"function": {
|
|
"name": "get_weather",
|
|
"parameters": {"type": "object", "properties": {}}
|
|
}
|
|
}
|
|
]
|
|
}
|
|
|
|
with pytest.raises(litellm.BadRequestError) as exc_info:
|
|
config.map_openai_params(
|
|
non_default_params=non_default_params,
|
|
optional_params={},
|
|
model=model,
|
|
drop_params=False,
|
|
)
|
|
|
|
assert "does not support native structured outputs" in str(exc_info.value)
|
|
assert "incompatible with user-provided tools" in str(exc_info.value)
|
|
|
|
def test_structured_output_without_tools_uses_workaround_for_non_native_models(self):
|
|
"""
|
|
Test that structured outputs without tools works using the json_tool_call workaround
|
|
for models that don't support native json_schema.
|
|
"""
|
|
config = GroqChatConfig()
|
|
|
|
model = "llama-3.3-70b-versatile"
|
|
|
|
non_default_params = {
|
|
"response_format": {
|
|
"type": "json_schema",
|
|
"json_schema": {
|
|
"name": "test",
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {"name": {"type": "string"}},
|
|
"required": ["name"]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
result = config.map_openai_params(
|
|
non_default_params=non_default_params,
|
|
optional_params={},
|
|
model=model,
|
|
drop_params=False,
|
|
)
|
|
|
|
# Should use the workaround (json_tool_call)
|
|
assert "tools" in result
|
|
assert len(result["tools"]) == 1
|
|
assert result["tools"][0]["function"]["name"] == "json_tool_call"
|
|
assert result["tool_choice"]["function"]["name"] == "json_tool_call"
|
|
assert result.get("json_mode") is True
|
|
|
|
def test_structured_output_passes_through_for_native_models(self):
|
|
"""
|
|
Test that structured outputs pass through directly for models that
|
|
support native json_schema (e.g., gpt-oss-120b).
|
|
"""
|
|
config = GroqChatConfig()
|
|
|
|
# Model that supports native json_schema
|
|
model = "openai/gpt-oss-120b"
|
|
|
|
non_default_params = {
|
|
"response_format": {
|
|
"type": "json_schema",
|
|
"json_schema": {
|
|
"name": "test",
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {"name": {"type": "string"}},
|
|
"required": ["name"]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
result = config.map_openai_params(
|
|
non_default_params=non_default_params,
|
|
optional_params={},
|
|
model=model,
|
|
drop_params=False,
|
|
)
|
|
|
|
# Should NOT use the workaround - response_format should pass through
|
|
# The workaround sets json_mode=True, so if it's not set, we know it passed through
|
|
assert result.get("json_mode") is not True
|
|
# Should not have the json_tool_call tool
|
|
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"
|