feat(groq): add support for json_schema (#32396)

This commit is contained in:
Jan Z
2025-09-09 11:30:07 -07:00
committed by GitHub
parent 4c6af2d1b2
commit 08bf4c321f
2 changed files with 130 additions and 41 deletions

View File

@@ -51,6 +51,7 @@ from langchain_core.runnables import Runnable, RunnableMap, RunnablePassthrough
from langchain_core.tools import BaseTool
from langchain_core.utils import from_env, get_pydantic_field_names, secret_from_env
from langchain_core.utils.function_calling import (
convert_to_json_schema,
convert_to_openai_function,
convert_to_openai_tool,
)
@@ -503,8 +504,13 @@ class ChatGroq(BaseChatModel):
async_api=async_api, run_manager=run_manager, **kwargs
)
if base_should_stream and ("response_format" in kwargs):
# Streaming not supported in JSON mode.
return kwargs["response_format"] != {"type": "json_object"}
# Streaming not supported in JSON mode or structured outputs.
response_format = kwargs["response_format"]
if isinstance(response_format, dict) and response_format.get("type") in {
"json_schema",
"json_object",
}:
return False
return base_should_stream
def _generate(
@@ -850,7 +856,9 @@ class ChatGroq(BaseChatModel):
self,
schema: Optional[Union[dict, type[BaseModel]]] = None,
*,
method: Literal["function_calling", "json_mode"] = "function_calling",
method: Literal[
"function_calling", "json_mode", "json_schema"
] = "function_calling",
include_raw: bool = False,
**kwargs: Any,
) -> Runnable[LanguageModelInput, dict | BaseModel]:
@@ -875,12 +883,34 @@ class ChatGroq(BaseChatModel):
Added support for TypedDict class.
.. versionchanged:: 0.3.8
Added support for Groq's dedicated structured output feature via
``method="json_schema"``.
method: The method for steering model generation, one of:
- ``'function_calling'``:
Uses Groq's tool-calling `API <https://console.groq.com/docs/tool-use>`__
- ``'json_schema'``:
Uses Groq's `Structured Output API <https://console.groq.com/docs/structured-outputs>`__.
Supported for a subset of models, including ``openai/gpt-oss``,
``moonshotai/kimi-k2-instruct``, and some ``meta-llama/llama-4``
models. See `docs <https://console.groq.com/docs/structured-outputs>`__
for details.
- ``'json_mode'``:
Uses Groq's `JSON mode <https://console.groq.com/docs/structured-outputs#json-object-mode>`__.
Note that if using JSON mode then you must include instructions for
formatting the output into the desired schema into the model call
Learn more about the differences between the methods and which models
support which methods `here <https://console.groq.com/docs/structured-outputs>`__.
method:
The method for steering model generation, either ``'function_calling'``
or ``'json_mode'``. If ``'function_calling'`` then the schema will be converted
to an OpenAI function and the returned model will make use of the
function-calling API. If ``'json_mode'`` then OpenAI's JSON mode will be
used.
function-calling API. If ``'json_mode'`` then JSON mode will be used.
.. note::
If using ``'json_mode'`` then you must include instructions for formatting
@@ -938,7 +968,7 @@ class ChatGroq(BaseChatModel):
)
llm = ChatGroq(model="llama-3.1-405b-reasoning", temperature=0)
llm = ChatGroq(model="openai/gpt-oss-120b", temperature=0)
structured_llm = llm.with_structured_output(AnswerWithJustification)
structured_llm.invoke(
@@ -964,7 +994,7 @@ class ChatGroq(BaseChatModel):
justification: str
llm = ChatGroq(model="llama-3.1-405b-reasoning", temperature=0)
llm = ChatGroq(model="openai/gpt-oss-120b", temperature=0)
structured_llm = llm.with_structured_output(
AnswerWithJustification, include_raw=True
)
@@ -997,7 +1027,7 @@ class ChatGroq(BaseChatModel):
]
llm = ChatGroq(model="llama-3.1-405b-reasoning", temperature=0)
llm = ChatGroq(model="openai/gpt-oss-120b", temperature=0)
structured_llm = llm.with_structured_output(AnswerWithJustification)
structured_llm.invoke(
@@ -1026,7 +1056,7 @@ class ChatGroq(BaseChatModel):
}
}
llm = ChatGroq(model="llama-3.1-405b-reasoning", temperature=0)
llm = ChatGroq(model="openai/gpt-oss-120b", temperature=0)
structured_llm = llm.with_structured_output(oai_schema)
structured_llm.invoke(
@@ -1037,6 +1067,41 @@ class ChatGroq(BaseChatModel):
# 'justification': 'Both a pound of bricks and a pound of feathers weigh one pound. The weight is the same, but the volume and density of the two substances differ.'
# }
Example: schema=Pydantic class, method="json_schema", include_raw=False:
.. code-block:: python
from typing import Optional
from langchain_groq import ChatGroq
from pydantic import BaseModel, Field
class AnswerWithJustification(BaseModel):
'''An answer to the user question along with justification for the answer.'''
answer: str
# If we provide default values and/or descriptions for fields, these will be passed
# to the model. This is an important part of improving a model's ability to
# correctly return structured outputs.
justification: Optional[str] = Field(
default=None, description="A justification for the answer."
)
llm = ChatGroq(model="openai/gpt-oss-120b", temperature=0)
structured_llm = llm.with_structured_output(
AnswerWithJustification, method="json_schema"
)
structured_llm.invoke(
"What weighs more a pound of bricks or a pound of feathers"
)
# -> AnswerWithJustification(
# answer='They weigh the same',
# justification='Both a pound of bricks and a pound of feathers weigh one pound. The weight is the same, but the volume or density of the objects may differ.'
# )
Example: schema=Pydantic class, method="json_mode", include_raw=True:
.. code-block::
@@ -1047,7 +1112,7 @@ class ChatGroq(BaseChatModel):
answer: str
justification: str
llm = ChatGroq(model="llama-3.1-405b-reasoning", temperature=0)
llm = ChatGroq(model="openai/gpt-oss-120b", temperature=0)
structured_llm = llm.with_structured_output(
AnswerWithJustification,
method="json_mode",
@@ -1065,35 +1130,12 @@ class ChatGroq(BaseChatModel):
# 'parsing_error': None
# }
Example: schema=None, method="json_mode", include_raw=True:
.. code-block::
structured_llm = llm.with_structured_output(method="json_mode", include_raw=True)
structured_llm.invoke(
"Answer the following question. "
"Make sure to return a JSON blob with keys 'answer' and 'justification'.\n\n"
"What's heavier a pound of bricks or a pound of feathers?"
)
# -> {
# 'raw': AIMessage(content='{\n "answer": "They are both the same weight.",\n "justification": "Both a pound of bricks and a pound of feathers weigh one pound. The difference lies in the volume and density of the materials, not the weight." \n}'),
# 'parsed': {
# 'answer': 'They are both the same weight.',
# 'justification': 'Both a pound of bricks and a pound of feathers weigh one pound. The difference lies in the volume and density of the materials, not the weight.'
# },
# 'parsing_error': None
# }
""" # noqa: E501
_ = kwargs.pop("strict", None)
if kwargs:
msg = f"Received unsupported arguments {kwargs}"
raise ValueError(msg)
is_pydantic_schema = _is_pydantic_class(schema)
if method == "json_schema":
# Some applications require that incompatible parameters (e.g., unsupported
# methods) be handled.
method = "function_calling"
if method == "function_calling":
if schema is None:
msg = (
@@ -1120,6 +1162,35 @@ class ChatGroq(BaseChatModel):
output_parser = JsonOutputKeyToolsParser(
key_name=tool_name, first_tool_only=True
)
elif method == "json_schema":
# Use structured outputs (json_schema) for models that support it
# Convert schema to JSON Schema format for structured outputs
if schema is None:
msg = (
"schema must be specified when method is 'json_schema'. "
"Received None."
)
raise ValueError(msg)
json_schema = convert_to_json_schema(schema)
schema_name = json_schema.get("title", "")
response_format = {
"type": "json_schema",
"json_schema": {"name": schema_name, "schema": json_schema},
}
ls_format_info = {
"kwargs": {"method": "json_schema"},
"schema": json_schema,
}
llm = self.bind(
response_format=response_format,
ls_structured_output_format=ls_format_info,
)
output_parser = (
PydanticOutputParser(pydantic_object=schema) # type: ignore[type-var, arg-type]
if is_pydantic_schema
else JsonOutputParser()
)
elif method == "json_mode":
llm = self.bind(
response_format={"type": "json_object"},

View File

@@ -1,5 +1,7 @@
"""Standard LangChain interface tests."""
from typing import Literal
import pytest
from langchain_core.language_models import BaseChatModel
from langchain_core.rate_limiters import InMemoryRateLimiter
@@ -13,11 +15,15 @@ from langchain_groq import ChatGroq
rate_limiter = InMemoryRateLimiter(requests_per_second=0.2)
class BaseTestGroq(ChatModelIntegrationTests):
class TestGroq(ChatModelIntegrationTests):
@property
def chat_model_class(self) -> type[BaseChatModel]:
return ChatGroq
@property
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
@@ -29,11 +35,23 @@ class BaseTestGroq(ChatModelIntegrationTests):
return True
class TestGroqGemma(BaseTestGroq):
@property
def chat_model_params(self) -> dict:
return {"model": "gemma2-9b-it", "rate_limiter": rate_limiter}
@pytest.mark.parametrize("schema_type", ["pydantic", "typeddict", "json_schema"])
def test_json_schema(
schema_type: Literal["pydantic", "typeddict", "json_schema"],
) -> None:
class JsonSchemaTests(ChatModelIntegrationTests):
@property
def chat_model_class(self) -> type[ChatGroq]:
return ChatGroq
@property
def supports_json_mode(self) -> bool:
return True
@property
def chat_model_params(self) -> dict:
return {"model": "openai/gpt-oss-120b", "rate_limiter": rate_limiter}
@property
def structured_output_kwargs(self) -> dict:
return {"method": "json_schema"}
test_instance = JsonSchemaTests()
model = test_instance.chat_model_class(**test_instance.chat_model_params)
JsonSchemaTests().test_structured_output(model, schema_type)