mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-12 23:01:41 +00:00
fix(groq): strip assistant provider_specific_fields in message transform
This commit is contained in:
parent
5544803b35
commit
fe656d8d4f
2 changed files with 76 additions and 27 deletions
|
|
@ -1,6 +1,7 @@
|
|||
"""
|
||||
Translate from OpenAI's `/v1/chat/completions` to Groq's `/v1/chat/completions`
|
||||
"""
|
||||
|
||||
from typing import (
|
||||
Any,
|
||||
Coroutine,
|
||||
|
|
@ -115,8 +116,7 @@ class GroqChatConfig(OpenAILikeChatConfig):
|
|||
@overload
|
||||
def _transform_messages(
|
||||
self, messages: List[AllMessageValues], model: str, is_async: Literal[True]
|
||||
) -> Coroutine[Any, Any, List[AllMessageValues]]:
|
||||
...
|
||||
) -> Coroutine[Any, Any, List[AllMessageValues]]: ...
|
||||
|
||||
@overload
|
||||
def _transform_messages(
|
||||
|
|
@ -124,8 +124,7 @@ class GroqChatConfig(OpenAILikeChatConfig):
|
|||
messages: List[AllMessageValues],
|
||||
model: str,
|
||||
is_async: Literal[False] = False,
|
||||
) -> List[AllMessageValues]:
|
||||
...
|
||||
) -> List[AllMessageValues]: ...
|
||||
|
||||
def _transform_messages(
|
||||
self, messages: List[AllMessageValues], model: str, is_async: bool = False
|
||||
|
|
@ -133,6 +132,8 @@ class GroqChatConfig(OpenAILikeChatConfig):
|
|||
for idx, message in enumerate(messages):
|
||||
"""
|
||||
1. Don't pass 'null' function_call assistant message to groq - https://github.com/BerriAI/litellm/issues/5839
|
||||
2. Strip LiteLLM-internal provider_specific_fields from assistant history
|
||||
before sending to Groq, which rejects unknown assistant properties.
|
||||
"""
|
||||
if isinstance(message, BaseModel):
|
||||
_message = message.model_dump()
|
||||
|
|
@ -142,6 +143,8 @@ class GroqChatConfig(OpenAILikeChatConfig):
|
|||
if assistant_message:
|
||||
new_message = ChatCompletionAssistantMessage(role="assistant")
|
||||
for k, v in _message.items():
|
||||
if k == "provider_specific_fields":
|
||||
continue
|
||||
if v is not None:
|
||||
new_message[k] = v # type: ignore
|
||||
messages[idx] = new_message
|
||||
|
|
@ -293,10 +296,10 @@ class GroqChatConfig(OpenAILikeChatConfig):
|
|||
json_mode=json_mode,
|
||||
)
|
||||
|
||||
mapped_service_tier: Literal[
|
||||
"auto", "default", "flex"
|
||||
] = self._map_groq_service_tier(
|
||||
original_service_tier=getattr(model_response, "service_tier")
|
||||
mapped_service_tier: Literal["auto", "default", "flex"] = (
|
||||
self._map_groq_service_tier(
|
||||
original_service_tier=getattr(model_response, "service_tier")
|
||||
)
|
||||
)
|
||||
setattr(model_response, "service_tier", mapped_service_tier)
|
||||
return model_response
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import os
|
||||
import sys
|
||||
from typing import List, cast
|
||||
|
||||
|
||||
import pytest
|
||||
|
|
@ -15,6 +16,8 @@ from litellm.llms.groq.chat.transformation import (
|
|||
GroqChatConfig,
|
||||
GroqChatCompletionStreamingHandler,
|
||||
)
|
||||
from litellm.types.llms.openai import AllMessageValues
|
||||
|
||||
|
||||
class TestGroq(BaseLLMChatTest):
|
||||
def get_base_completion_call_args(self) -> dict:
|
||||
|
|
@ -29,12 +32,41 @@ class TestGroq(BaseLLMChatTest):
|
|||
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"])
|
||||
@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
|
||||
|
||||
def test_transform_messages_strips_assistant_provider_specific_fields(self):
|
||||
"""Groq rejects unknown assistant fields like provider_specific_fields."""
|
||||
config = GroqChatConfig()
|
||||
messages = cast(
|
||||
List[AllMessageValues],
|
||||
[
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "Tool metadata",
|
||||
"provider_specific_fields": {
|
||||
"mcp_list_tools": [{"name": "weather"}],
|
||||
"mcp_tool_calls": [{"id": "call_123"}],
|
||||
},
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
transformed = cast(
|
||||
List[AllMessageValues],
|
||||
config._transform_messages(messages=messages, model="qwen/qwen3-32b"),
|
||||
)
|
||||
|
||||
assert transformed[0]["role"] == "assistant"
|
||||
assert transformed[0].get("content") == "Tool metadata"
|
||||
assert "provider_specific_fields" not in transformed[0]
|
||||
|
||||
|
||||
class TestGroqStructuredOutputs:
|
||||
"""
|
||||
|
|
@ -66,19 +98,19 @@ class TestGroqStructuredOutputs:
|
|||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {"name": {"type": "string"}},
|
||||
"required": ["name"]
|
||||
}
|
||||
}
|
||||
"required": ["name"],
|
||||
},
|
||||
},
|
||||
},
|
||||
"tools": [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_weather",
|
||||
"parameters": {"type": "object", "properties": {}}
|
||||
}
|
||||
"parameters": {"type": "object", "properties": {}},
|
||||
},
|
||||
}
|
||||
]
|
||||
],
|
||||
}
|
||||
|
||||
with pytest.raises(litellm.BadRequestError) as exc_info:
|
||||
|
|
@ -92,7 +124,9 @@ class TestGroqStructuredOutputs:
|
|||
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):
|
||||
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.
|
||||
|
|
@ -109,9 +143,9 @@ class TestGroqStructuredOutputs:
|
|||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {"name": {"type": "string"}},
|
||||
"required": ["name"]
|
||||
}
|
||||
}
|
||||
"required": ["name"],
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -147,9 +181,9 @@ class TestGroqStructuredOutputs:
|
|||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {"name": {"type": "string"}},
|
||||
"required": ["name"]
|
||||
}
|
||||
}
|
||||
"required": ["name"],
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -172,7 +206,7 @@ class TestGroqStructuredOutputs:
|
|||
class TestGroqReasoning:
|
||||
"""
|
||||
Tests for Groq reasoning field mapping.
|
||||
|
||||
|
||||
Groq returns 'reasoning' field in delta, but LiteLLM expects 'reasoning_content'.
|
||||
"""
|
||||
|
||||
|
|
@ -207,7 +241,10 @@ class TestGroqReasoning:
|
|||
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"
|
||||
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")
|
||||
|
||||
|
|
@ -268,7 +305,10 @@ class TestGroqReasoning:
|
|||
{
|
||||
"index": 0,
|
||||
"id": "call_123",
|
||||
"function": {"name": "test_function", "arguments": "{}"},
|
||||
"function": {
|
||||
"name": "test_function",
|
||||
"arguments": "{}",
|
||||
},
|
||||
"type": "function",
|
||||
}
|
||||
],
|
||||
|
|
@ -283,8 +323,14 @@ class TestGroqReasoning:
|
|||
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"
|
||||
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"
|
||||
assert (
|
||||
parsed_chunk.choices[0].delta.tool_calls[0]["function"]["name"]
|
||||
== "test_function"
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue