Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion libs/partners/groq/langchain_groq/chat_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,16 @@
ToolCall,
ToolMessage,
ToolMessageChunk,
is_data_content_block,
)
from langchain_core.messages.ai import (
InputTokenDetails,
OutputTokenDetails,
UsageMetadata,
)
from langchain_core.messages.block_translators.openai import (
convert_to_openai_data_block,
)
from langchain_core.output_parsers import JsonOutputParser, PydanticOutputParser
from langchain_core.output_parsers.base import OutputParserLike
from langchain_core.output_parsers.openai_tools import (
Expand Down Expand Up @@ -1263,6 +1267,29 @@ def _is_pydantic_class(obj: Any) -> bool:
#
# Type conversion helpers
#
def _format_message_content(content: Any) -> Any:
"""Format message content for Groq API.

Converts LangChain image content blocks to Groq's expected image_url format.

Args:
content: The message content (string or list of content blocks).

Returns:
Formatted content suitable for Groq API.
"""
if content and isinstance(content, list):
formatted: list = []
for block in content:
# Handle LangChain standard data content blocks (image, audio, file)
if isinstance(block, dict) and is_data_content_block(block):
formatted.append(convert_to_openai_data_block(block))
else:
formatted.append(block)
return formatted
return content


def _convert_message_to_dict(message: BaseMessage) -> dict:
"""Convert a LangChain message to a dictionary.

Expand All @@ -1277,7 +1304,10 @@ def _convert_message_to_dict(message: BaseMessage) -> dict:
if isinstance(message, ChatMessage):
message_dict = {"role": message.role, "content": message.content}
elif isinstance(message, HumanMessage):
message_dict = {"role": "user", "content": message.content}
message_dict = {
"role": "user",
"content": _format_message_content(message.content),
}
elif isinstance(message, AIMessage):
# Translate v1 content
if message.response_metadata.get("output_version") == "v1":
Expand Down
7 changes: 0 additions & 7 deletions libs/partners/groq/tests/integration_tests/test_standard.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import pytest
from langchain_core.language_models import BaseChatModel
from langchain_core.rate_limiters import InMemoryRateLimiter
from langchain_core.tools import BaseTool
from langchain_tests.integration_tests import (
ChatModelIntegrationTests,
)
Expand All @@ -24,12 +23,6 @@ def chat_model_class(self) -> type[BaseChatModel]:
def chat_model_params(self) -> dict:
return {"model": "llama-3.3-70b-versatile", "rate_limiter": rate_limiter}

@pytest.mark.xfail(reason="Not yet implemented.")
def test_tool_message_histories_list_content(
self, model: BaseChatModel, my_adder_tool: BaseTool
) -> None:
super().test_tool_message_histories_list_content(model, my_adder_tool)

@pytest.mark.xfail(
reason="Groq models have inconsistent tool calling performance. See: "
"https://github.com/langchain-ai/langchain/discussions/19990"
Expand Down
62 changes: 62 additions & 0 deletions libs/partners/groq/tests/unit_tests/test_chat_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
_convert_chunk_to_message_chunk,
_convert_dict_to_message,
_create_usage_metadata,
_format_message_content,
)

if "GROQ_API_KEY" not in os.environ:
Expand Down Expand Up @@ -945,3 +946,64 @@ def test_combine_llm_outputs_with_missing_details() -> None:
def test_profile() -> None:
model = ChatGroq(model="openai/gpt-oss-20b")
assert model.profile


def test_format_message_content_string() -> None:
"""Test that string content is passed through unchanged."""
content = "hello"
assert content == _format_message_content(content)


def test_format_message_content_none() -> None:
"""Test that None content is passed through unchanged."""
content = None
assert content == _format_message_content(content)


def test_format_message_content_empty_list() -> None:
"""Test that empty list is passed through unchanged."""
content: list = []
assert content == _format_message_content(content)


def test_format_message_content_text_and_image_url() -> None:
"""Test that existing image_url format is passed through unchanged."""
content = [
{"type": "text", "text": "What is in this image?"},
{"type": "image_url", "image_url": {"url": "https://example.com/image.jpg"}},
]
assert content == _format_message_content(content)


def test_format_message_content_langchain_image_base64() -> None:
"""Test that LangChain image blocks with base64 are converted."""
content = {"type": "image", "base64": "<base64 data>", "mime_type": "image/png"}
expected = [
{
"type": "image_url",
"image_url": {"url": "data:image/png;base64,<base64 data>"},
}
]
assert expected == _format_message_content([content])


def test_format_message_content_langchain_image_url() -> None:
"""Test that LangChain image blocks with URL are converted."""
content = {"type": "image", "url": "https://example.com/image.jpg"}
expected = [
{"type": "image_url", "image_url": {"url": "https://example.com/image.jpg"}}
]
assert expected == _format_message_content([content])


def test_format_message_content_mixed() -> None:
"""Test that mixed content with text and image is handled correctly."""
content = [
{"type": "text", "text": "Describe this image"},
{"type": "image", "base64": "<data>", "mime_type": "image/png"},
]
expected = [
{"type": "text", "text": "Describe this image"},
{"type": "image_url", "image_url": {"url": "data:image/png;base64,<data>"}},
]
assert expected == _format_message_content(content)
Loading