Skip to content

Commit fb490b0

Browse files
authored
feat(groq): losen restrictions on reasoning_effort, inject effort in meta, update tests (#32415)
1 parent 419c173 commit fb490b0

File tree

2 files changed

+98
-36
lines changed

2 files changed

+98
-36
lines changed

libs/partners/groq/langchain_groq/chat_models.py

Lines changed: 22 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,7 @@
66
import warnings
77
from collections.abc import AsyncIterator, Iterator, Mapping, Sequence
88
from operator import itemgetter
9-
from typing import (
10-
Any,
11-
Callable,
12-
Literal,
13-
Optional,
14-
TypedDict,
15-
Union,
16-
cast,
17-
)
9+
from typing import Any, Callable, Literal, Optional, TypedDict, Union, cast
1810

1911
from langchain_core._api import deprecated
2012
from langchain_core.callbacks import (
@@ -46,10 +38,7 @@
4638
ToolMessage,
4739
ToolMessageChunk,
4840
)
49-
from langchain_core.output_parsers import (
50-
JsonOutputParser,
51-
PydanticOutputParser,
52-
)
41+
from langchain_core.output_parsers import JsonOutputParser, PydanticOutputParser
5342
from langchain_core.output_parsers.base import OutputParserLike
5443
from langchain_core.output_parsers.openai_tools import (
5544
JsonOutputKeyToolsParser,
@@ -60,23 +49,13 @@
6049
from langchain_core.outputs import ChatGeneration, ChatGenerationChunk, ChatResult
6150
from langchain_core.runnables import Runnable, RunnableMap, RunnablePassthrough
6251
from langchain_core.tools import BaseTool
63-
from langchain_core.utils import (
64-
from_env,
65-
get_pydantic_field_names,
66-
secret_from_env,
67-
)
52+
from langchain_core.utils import from_env, get_pydantic_field_names, secret_from_env
6853
from langchain_core.utils.function_calling import (
6954
convert_to_openai_function,
7055
convert_to_openai_tool,
7156
)
7257
from langchain_core.utils.pydantic import is_basemodel_subclass
73-
from pydantic import (
74-
BaseModel,
75-
ConfigDict,
76-
Field,
77-
SecretStr,
78-
model_validator,
79-
)
58+
from pydantic import BaseModel, ConfigDict, Field, SecretStr, model_validator
8059
from typing_extensions import Self
8160

8261
from langchain_groq.version import __version__
@@ -122,7 +101,7 @@ class ChatGroq(BaseChatModel):
122101
123102
See the `Groq documentation
124103
<https://console.groq.com/docs/reasoning#reasoning>`__ for more
125-
details and a list of supported reasoning models.
104+
details and a list of supported models.
126105
model_kwargs: Dict[str, Any]
127106
Holds any model parameters valid for create call not
128107
explicitly specified.
@@ -328,20 +307,15 @@ class Joke(BaseModel):
328307
overridden in ``reasoning_effort``.
329308
330309
See the `Groq documentation <https://console.groq.com/docs/reasoning#reasoning>`__
331-
for more details and a list of supported reasoning models.
310+
for more details and a list of supported models.
332311
"""
333-
reasoning_effort: Optional[Literal["none", "default"]] = Field(default=None)
312+
reasoning_effort: Optional[str] = Field(default=None)
334313
"""The level of effort the model will put into reasoning. Groq will default to
335-
enabling reasoning if left undefined. If set to ``none``, ``reasoning_format`` will
336-
not apply and ``reasoning_content`` will not be returned.
337-
338-
- ``'none'``: Disable reasoning. The model will not use any reasoning tokens when
339-
generating a response.
340-
- ``'default'``: Enable reasoning.
314+
enabling reasoning if left undefined.
341315
342316
See the `Groq documentation
343317
<https://console.groq.com/docs/reasoning#options-for-reasoning-effort>`__ for more
344-
details and a list of models that support setting a reasoning effort.
318+
details and a list of options and models that support setting a reasoning effort.
345319
"""
346320
model_kwargs: dict[str, Any] = Field(default_factory=dict)
347321
"""Holds any model parameters valid for `create` call not explicitly specified."""
@@ -601,6 +575,11 @@ def _stream(
601575
generation_info["system_fingerprint"] = system_fingerprint
602576
service_tier = params.get("service_tier") or self.service_tier
603577
generation_info["service_tier"] = service_tier
578+
reasoning_effort = (
579+
params.get("reasoning_effort") or self.reasoning_effort
580+
)
581+
if reasoning_effort:
582+
generation_info["reasoning_effort"] = reasoning_effort
604583
logprobs = choice.get("logprobs")
605584
if logprobs:
606585
generation_info["logprobs"] = logprobs
@@ -644,6 +623,11 @@ async def _astream(
644623
generation_info["system_fingerprint"] = system_fingerprint
645624
service_tier = params.get("service_tier") or self.service_tier
646625
generation_info["service_tier"] = service_tier
626+
reasoning_effort = (
627+
params.get("reasoning_effort") or self.reasoning_effort
628+
)
629+
if reasoning_effort:
630+
generation_info["reasoning_effort"] = reasoning_effort
647631
logprobs = choice.get("logprobs")
648632
if logprobs:
649633
generation_info["logprobs"] = logprobs
@@ -714,6 +698,9 @@ def _create_chat_result(
714698
"system_fingerprint": response.get("system_fingerprint", ""),
715699
}
716700
llm_output["service_tier"] = params.get("service_tier") or self.service_tier
701+
reasoning_effort = params.get("reasoning_effort") or self.reasoning_effort
702+
if reasoning_effort:
703+
llm_output["reasoning_effort"] = reasoning_effort
717704
return ChatResult(generations=generations, llm_output=llm_output)
718705

719706
def _create_message_dicts(

libs/partners/groq/tests/integration_tests/test_chat_models.py

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
)
2626

2727
DEFAULT_MODEL_NAME = "openai/gpt-oss-20b"
28+
29+
# gpt-oss doesn't support `reasoning_effort`
2830
REASONING_MODEL_NAME = "deepseek-r1-distill-llama-70b"
2931

3032

@@ -272,7 +274,7 @@ def test_reasoning_output_stream() -> None:
272274
def test_reasoning_effort_none() -> None:
273275
"""Test that no reasoning output is returned if effort is set to none."""
274276
chat = ChatGroq(
275-
model="qwen/qwen3-32b", # Only qwen3 currently supports reasoning_effort
277+
model="qwen/qwen3-32b", # Only qwen3 currently supports reasoning_effort = none
276278
reasoning_effort="none",
277279
)
278280
message = HumanMessage(content="What is the capital of France?")
@@ -282,6 +284,79 @@ def test_reasoning_effort_none() -> None:
282284
assert "<think>" not in response.content and "<think/>" not in response.content
283285

284286

287+
@pytest.mark.parametrize("effort", ["low", "medium", "high"])
288+
def test_reasoning_effort_levels(effort: str) -> None:
289+
"""Test reasoning effort options for different levels."""
290+
# As of now, only the new gpt-oss models support `'low'`, `'medium'`, and `'high'`
291+
chat = ChatGroq(
292+
model=DEFAULT_MODEL_NAME,
293+
reasoning_effort=effort,
294+
)
295+
message = HumanMessage(content="What is the capital of France?")
296+
response = chat.invoke([message])
297+
assert isinstance(response, AIMessage)
298+
assert isinstance(response.content, str)
299+
assert len(response.content) > 0
300+
assert response.response_metadata.get("reasoning_effort") == effort
301+
302+
303+
@pytest.mark.parametrize("effort", ["low", "medium", "high"])
304+
def test_reasoning_effort_invoke_override(effort: str) -> None:
305+
"""Test that reasoning_effort in invoke() overrides class-level setting."""
306+
# Create chat with no reasoning effort at class level
307+
chat = ChatGroq(
308+
model=DEFAULT_MODEL_NAME,
309+
)
310+
message = HumanMessage(content="What is the capital of France?")
311+
312+
# Override reasoning_effort in invoke()
313+
response = chat.invoke([message], reasoning_effort=effort)
314+
assert isinstance(response, AIMessage)
315+
assert isinstance(response.content, str)
316+
assert len(response.content) > 0
317+
assert response.response_metadata.get("reasoning_effort") == effort
318+
319+
320+
def test_reasoning_effort_invoke_override_different_level() -> None:
321+
"""Test that reasoning_effort in invoke() overrides class-level setting."""
322+
# Create chat with reasoning effort at class level
323+
chat = ChatGroq(
324+
model=DEFAULT_MODEL_NAME, # openai/gpt-oss-20b supports reasoning_effort
325+
reasoning_effort="high",
326+
)
327+
message = HumanMessage(content="What is the capital of France?")
328+
329+
# Override reasoning_effort to 'low' in invoke()
330+
response = chat.invoke([message], reasoning_effort="low")
331+
assert isinstance(response, AIMessage)
332+
assert isinstance(response.content, str)
333+
assert len(response.content) > 0
334+
# Should reflect the overridden value, not the class-level setting
335+
assert response.response_metadata.get("reasoning_effort") == "low"
336+
337+
338+
def test_reasoning_effort_streaming() -> None:
339+
"""Test that reasoning_effort is captured in streaming response metadata."""
340+
chat = ChatGroq(
341+
model=DEFAULT_MODEL_NAME,
342+
reasoning_effort="medium",
343+
)
344+
message = HumanMessage(content="What is the capital of France?")
345+
346+
chunks = list(chat.stream([message]))
347+
assert len(chunks) > 0
348+
349+
# Find the final chunk with finish_reason
350+
final_chunk = None
351+
for chunk in chunks:
352+
if chunk.response_metadata.get("finish_reason"):
353+
final_chunk = chunk
354+
break
355+
356+
assert final_chunk is not None
357+
assert final_chunk.response_metadata.get("reasoning_effort") == "medium"
358+
359+
285360
#
286361
# Misc tests
287362
#

0 commit comments

Comments
 (0)