feat(core): add reasoning_effort as a standard chat model parameter (#38887)

`langchain-anthropic`, `langchain-openai`, `langchain-fireworks`, and
`langchain-xai` now support a standard `reasoning_effort` parameter. It
can be set at model construction or per invocation, with each provider
translating it to the appropriate API.
`ModelProfile` now exposes supported reasoning effort levels and the
default level for supported models.

---

Adds a standard `reasoning_effort` parameter across chat model
integrations. Like `temperature`, it can be set on the model or per
call, while each provider translates it into its own API format.

Motivating example: [`deepagents-code`
(dcode)](https://docs.langchain.com/oss/python/deepagents/code/overview)
implemented provider-agnostic reasoning effort itself. This PR upstreams
that support into LangChain's provider integrations, making
`reasoning_effort` a standard parameter and exposing supported levels
via `ModelProfile`.

```python
from langchain_anthropic import ChatAnthropic

model = ChatAnthropic(model="claude-sonnet-4-6")
model.invoke(
    "Why do parrots have colorful feathers?",
    reasoning_effort="high",
)
```

### Model specific labels and default

| Provider | Model / version | Levels supported | Default |
|----------|-----------------|------------------|---------|
| **OpenAI** | gpt-5.5, gpt-5.6* | `none`, `low`, `medium`, `high`,
`xhigh` (+ `max` on gpt-5.6+) | `medium` |
| | other gpt-5* | `none`, `low`, `medium`, `high`, `xhigh` | — |
| **Anthropic** | Opus 4.0 / 4.1 | *(none – predates effort entirely)* |
— |
| | Opus 4.5 | `low`, `medium`, `high` | `high` |
| | Opus 4.6 | `low`, `medium`, `high`, `max` | `high` |
| | Opus 4.7+ | `low`, `medium`, `high`, `xhigh`, `max` | `high` |
| | Sonnet 4.0 / 4.1 / 4.5 | *(none – predates or rejects effort)* | — |
| | Sonnet 4.6 | `low`, `medium`, `high`, `max` | `high` |
| | Sonnet 5+ | `low`, `medium`, `high`, `xhigh`, `max` | `high` |
| **Fireworks** | DeepSeek V4 Pro | `none`, `low`, `medium`, `high`,
`xhigh`, `max` | `high` |
| | Kimi K2 | `low`, `medium`, `high` | — |
| | GLM 5 | `none`, `high`, `max` | `max` |
| **xAI** | Grok 4.5-class models | `low`, `medium`, `high` | `high` |

### Provider specific details

| Provider | Model class | Translation |
|----------|-------------|-------------|
| **OpenAI** | `ChatOpenAI` | Translates `reasoning_effort` to
`reasoning.effort` and adds `summary: "auto"`. |
| **Anthropic** | `ChatAnthropic` | Translates `reasoning_effort` to
`output_config.effort` and defaults `thinking` to `adaptive`. |
| **Fireworks** | `ChatFireworks` | Sends `reasoning_effort` as a flat
field unchanged. |
| **xAI** | `ChatXAI` | Nests `reasoning_effort` under
`extra_body.reasoning_effort`. |

### Anthropic: `effort` alias

`ChatAnthropic` already exposed an `effort` parameter. This PR makes it
a true Pydantic alias for `reasoning_effort` while preserving existing
behavior.

- Both `effort` and `reasoning_effort` work at construction; if both are
provided, `effort` wins.
- Call-time (`.invoke()`, `.bind()`) support is handled manually to
preserve the same precedence, since Pydantic aliases only apply at
construction.
- `effort` remains supported (no deprecation).

### Other providers

- **DeepSeek**: inherits support from `BaseChatOpenAI`.
- **Groq** and **Perplexity**: already have native `reasoning_effort`
support.
- **OpenRouter** and **Mistral AI**: out of scope (no native support
today).
- **Google (Gemini)**: implemented separately in
[`langchain-google`](https://github.com/langchain-ai/langchain-google/pull/1895)
This commit is contained in:
Nishitha M authored and GitHub committed 2026-07-20 16:15:00 -04:00
1 parent 7d2bbba25f
commit 5ddfd0ed87
22 files changed
+1033 -16

No files matched your search

@@ -91,6 +91,22 @@ class ModelProfile(TypedDict, total=False):
reasoning_output: bool
"""Whether the model supports [reasoning / chain-of-thought](https://docs.langchain.com/oss/python/langchain/models#reasoning)."""
reasoning_effort_levels: list[str]
"""Supported reasoning-effort levels (e.g. `['low', 'medium', 'high']`).
Absent or empty if the model does not support a configurable reasoning
effort. Only meaningful when `reasoning_output` is `True`.
"""
reasoning_effort_default: str
"""The provider's documented default reasoning-effort level, if known.
Absent when no default is documented. Only meaningful when
`reasoning_effort_levels` is non-empty; not necessarily a member of
`reasoning_effort_levels` itself (a model may default to unconfigurable
behavior distinct from any explicit level).
"""
text_outputs: bool
"""Whether text outputs are supported."""
@@ -262,7 +262,7 @@ def test_configurable_with_default() -> None:
"max_tokens": 64000,
"temperature": None,
"thinking": None,
"effort": None,
"reasoning_effort": None,
"top_k": None,
"top_p": None,
"default_request_timeout": None,
@@ -304,7 +304,7 @@ def test_configurable_with_default() -> None:
"bound": {
"name": None,
"disable_streaming": False,
"effort": None,
"reasoning_effort": None,
"model": "claude-sonnet-4-5-20250929",
"mcp_servers": None,
"max_tokens": 64000,
@@ -806,6 +806,21 @@ def _is_code_execution_related_block(
return False
def _reasoning_effort_levels(profile: object) -> tuple[str, ...]:
"""Return the reasoning-effort levels declared in a model's profile, if any.
Defensive against a missing/malformed profile: an absent `profile`, a
non-mapping value, or a missing/non-list `reasoning_effort_levels` value is
treated as "no levels declared" rather than raising.
"""
if not isinstance(profile, Mapping):
return ()
levels = profile.get("reasoning_effort_levels")
if not isinstance(levels, (list, tuple)):
return ()
return tuple(levels)
def _is_direct_anthropic_llm_type(llm_type: object) -> bool:
"""Return whether an `_llm_type` reaches Claude via the direct Anthropic API.
@@ -1047,18 +1062,29 @@ class ChatAnthropic(BaseChatModel):
[extended output](https://platform.claude.com/docs/en/api/go/beta/messages/create).
"""
effort: Literal["max", "xhigh", "high", "medium", "low"] | None = None
"""Convenience shorthand for `output_config.effort`.
reasoning_effort: Literal["max", "xhigh", "high", "medium", "low"] | None = Field(
default=None,
alias="effort",
)
"""Reasoning effort.
When set, this value takes precedence over any `effort` key inside
`output_config`.
Configures `output_config.effort`. If `thinking` isn't set explicitly,
defaults it to `{"type": "adaptive", "display": "summarized"}`. Can also
be passed at call time (for example,
`model.invoke(..., reasoning_effort="high")`).
Example: `effort="medium"`
!!! note "`effort` alias"
`effort` is also accepted as an alias for this field, at both
construction and call time. If both `effort` and `reasoning_effort` are
set, `effort` wins (Pydantic's alias-resolution precedence).
!!! note
Setting `effort` to `'high'` produces exactly the same behavior as omitting the
parameter altogether.
Setting `reasoning_effort` to `'high'` produces exactly the same behavior
as omitting the parameter altogether.
Example: `reasoning_effort="medium"`
"""
mcp_servers: list[dict[str, Any]] | None = None
@@ -1089,6 +1115,11 @@ class ChatAnthropic(BaseChatModel):
docs for more information.
"""
@property
def effort(self) -> Literal["max", "xhigh", "high", "medium", "low"] | None:
"""Alias for `reasoning_effort`."""
return self.reasoning_effort
@property
def _llm_type(self) -> str:
"""Return type of chat model."""
@@ -1239,7 +1270,7 @@ class ChatAnthropic(BaseChatModel):
input_: LanguageModelInput,
*,
stop: list[str] | None = None,
**kwargs: dict,
**kwargs: Any,
) -> dict:
"""Get the request payload for the Anthropic API."""
messages = self._convert_input(input_).to_messages()
@@ -1312,26 +1343,56 @@ class ChatAnthropic(BaseChatModel):
**self.model_kwargs,
**kwargs,
}
# Captured before `self.thinking` is applied below, so a call-time
# `thinking` kwarg counts as "explicitly set" too.
thinking_explicitly_set = "thinking" in payload or self.thinking is not None
if self.thinking is not None:
payload["thinking"] = self.thinking
if self.inference_geo is not None:
payload["inference_geo"] = self.inference_geo
# Handle output_config and effort parameter
# Priority: self.effort > kwargs output_config > self.output_config
# Priority: kwarg `effort`/`reasoning_effort` > kwarg `output_config`
# > self.reasoning_effort > self.output_config
output_config: dict[str, Any] = {}
if self.output_config:
output_config.update(self.output_config)
reasoning_effort_applied = False
if self.reasoning_effort:
output_config["effort"] = self.reasoning_effort
reasoning_effort_applied = True
payload_oc = payload.get("output_config")
if isinstance(payload_oc, dict):
output_config.update(payload_oc)
if self.effort:
output_config["effort"] = self.effort
# Neither `reasoning_effort` nor its `effort` alias are Anthropic API
# fields. Pop them so they never leak through as top-level keys.
effort_kwarg = payload.pop("effort", None)
reasoning_effort_kwarg = payload.pop("reasoning_effort", None)
# `effort` wins if both are set at call time, matching the
# construction-time alias-resolution precedence (`Field(alias="effort")`).
reasoning_effort_override = (
effort_kwarg if effort_kwarg is not None else reasoning_effort_kwarg
)
if reasoning_effort_override:
output_config["effort"] = reasoning_effort_override
reasoning_effort_applied = True
if output_config:
payload["output_config"] = output_config
# Default adaptive thinking when `reasoning_effort` is set, unless the
# caller explicitly provided `thinking`. Gated on `xhigh` support: only
# Opus 4.7+/Sonnet 5 accept the adaptive+summarized `thinking` shape —
# sending it to an older model (e.g. Opus 4.5, 4.6) is rejected by the
# API with "adaptive thinking is not supported on this model".
if (
reasoning_effort_applied
and not thinking_explicitly_set
and "xhigh" in _reasoning_effort_levels(self.profile)
):
payload["thinking"] = {"type": "adaptive", "display": "summarized"}
if "response_format" in payload:
# response_format present when using agents.create_agent's ProviderStrategy
# ---
@@ -173,6 +173,12 @@ _PROFILES: dict[str, dict[str, Any]] = {
"pdf_tool_message": True,
"image_tool_message": True,
"tool_call_streaming": True,
"reasoning_effort_levels": [
"low",
"medium",
"high",
],
"reasoning_effort_default": "high",
},
"claude-opus-4-5-20251101": {
"name": "Claude Opus 4.5",
@@ -199,6 +205,12 @@ _PROFILES: dict[str, dict[str, Any]] = {
"pdf_tool_message": True,
"image_tool_message": True,
"tool_call_streaming": True,
"reasoning_effort_levels": [
"low",
"medium",
"high",
],
"reasoning_effort_default": "high",
},
"claude-opus-4-6": {
"name": "Claude Opus 4.6",
@@ -225,6 +237,13 @@ _PROFILES: dict[str, dict[str, Any]] = {
"pdf_tool_message": True,
"image_tool_message": True,
"tool_call_streaming": True,
"reasoning_effort_levels": [
"low",
"medium",
"high",
"max",
],
"reasoning_effort_default": "high",
},
"claude-opus-4-7": {
"name": "Claude Opus 4.7",
@@ -251,6 +270,14 @@ _PROFILES: dict[str, dict[str, Any]] = {
"pdf_tool_message": True,
"image_tool_message": True,
"tool_call_streaming": True,
"reasoning_effort_levels": [
"low",
"medium",
"high",
"xhigh",
"max",
],
"reasoning_effort_default": "high",
},
"claude-opus-4-8": {
"name": "Claude Opus 4.8",
@@ -277,6 +304,14 @@ _PROFILES: dict[str, dict[str, Any]] = {
"pdf_tool_message": True,
"image_tool_message": True,
"tool_call_streaming": True,
"reasoning_effort_levels": [
"low",
"medium",
"high",
"xhigh",
"max",
],
"reasoning_effort_default": "high",
},
"claude-sonnet-4-5": {
"name": "Claude Sonnet 4.5 (latest)",
@@ -355,6 +390,13 @@ _PROFILES: dict[str, dict[str, Any]] = {
"pdf_tool_message": True,
"image_tool_message": True,
"tool_call_streaming": True,
"reasoning_effort_levels": [
"low",
"medium",
"high",
"max",
],
"reasoning_effort_default": "high",
},
"claude-sonnet-5": {
"name": "Claude Sonnet 5",
@@ -381,5 +423,13 @@ _PROFILES: dict[str, dict[str, Any]] = {
"pdf_tool_message": True,
"image_tool_message": True,
"tool_call_streaming": True,
"reasoning_effort_levels": [
"low",
"medium",
"high",
"xhigh",
"max",
],
"reasoning_effort_default": "high",
},
}
@@ -16,15 +16,35 @@ structured_output = true
[overrides."claude-sonnet-4-6"]
structured_output = true
reasoning_effort_levels = ["low", "medium", "high", "max"]
reasoning_effort_default = "high"
[overrides."claude-opus-4-1"]
structured_output = true
[overrides."claude-opus-4-5"]
structured_output = true
reasoning_effort_levels = ["low", "medium", "high"]
reasoning_effort_default = "high"
[overrides."claude-opus-4-5-20251101"]
reasoning_effort_levels = ["low", "medium", "high"]
reasoning_effort_default = "high"
[overrides."claude-opus-4-6"]
structured_output = true
reasoning_effort_levels = ["low", "medium", "high", "max"]
reasoning_effort_default = "high"
[overrides."claude-opus-4-7"]
structured_output = true
reasoning_effort_levels = ["low", "medium", "high", "xhigh", "max"]
reasoning_effort_default = "high"
[overrides."claude-opus-4-8"]
reasoning_effort_levels = ["low", "medium", "high", "xhigh", "max"]
reasoning_effort_default = "high"
[overrides."claude-sonnet-5"]
reasoning_effort_levels = ["low", "medium", "high", "xhigh", "max"]
reasoning_effort_default = "high"
@@ -1321,6 +1321,65 @@ def test_effort_parameter() -> None:
assert result.usage_metadata["output_tokens"] > 0
def test_reasoning_effort_parameter() -> None:
"""Test that the standard `reasoning_effort` parameter is accepted by the API."""
llm = ChatAnthropic(
model="claude-opus-4-5-20251101",
reasoning_effort="medium",
max_tokens=100,
)
result = llm.invoke("Say hello in one sentence")
assert isinstance(result.content, str)
assert len(result.content) > 0
assert "model_name" in result.response_metadata
assert result.usage_metadata is not None
assert result.usage_metadata["input_tokens"] > 0
assert result.usage_metadata["output_tokens"] > 0
def test_reasoning_effort_call_time_kwarg() -> None:
"""Test that `reasoning_effort` is accepted as a call-time kwarg."""
llm = ChatAnthropic(model="claude-opus-4-5-20251101", max_tokens=100)
result = llm.invoke("Say hello in one sentence", reasoning_effort="low")
assert isinstance(result.content, str)
assert len(result.content) > 0
assert result.usage_metadata is not None
def test_reasoning_effort_defaults_adaptive_thinking() -> None:
"""`reasoning_effort` defaults `thinking` to adaptive on models that support it.
Regression test for a model (Opus 4.7+, Sonnet 5) actually accepting the
resulting `{"type": "adaptive", "display": "summarized"}` thinking config,
not just that the payload is well-formed locally.
"""
llm = ChatAnthropic(
model="claude-opus-4-7",
reasoning_effort="xhigh",
max_tokens=2_000,
)
# A genuine multi-step problem: adaptive thinking is model-decided, and a
# trivial prompt (e.g. "what is 3+4") may not surface a visible `thinking`
# block even when accepted, which would make this test flaky.
result = llm.invoke(
"A farmer has 17 sheep. All but 9 die. Then he buys triple the number "
"of remaining sheep, then sells half (rounding down). How many sheep "
"does he have now? Show your reasoning step by step."
)
assert isinstance(result.content, list)
assert any(
isinstance(block, dict) and block.get("type") == "thinking"
for block in result.content
)
assert result.usage_metadata is not None
def test_image_tool_calling() -> None:
"""Test tool calling with image inputs."""
@@ -2901,11 +2901,37 @@ def test_effort_in_output_config_payload() -> None:
model = ChatAnthropic(model="claude-opus-4-5-20251101", effort="medium")
assert model.effort == "medium"
# Test that effort is added to output_config
payload = model._get_request_payload("Test query")
assert payload["output_config"]["effort"] == "medium"
def test_effort_call_time_kwarg_does_not_warn() -> None:
"""Test that a call-time `effort` kwarg never warns.
`effort` is a permanent alias for `reasoning_effort`, not a deprecated one --
it's not being removed, so no warning is expected at construction or call
time.
"""
model = ChatAnthropic(model="claude-opus-4-5-20251101")
with warnings.catch_warnings():
warnings.simplefilter("error")
payload = model._get_request_payload("Test query", effort="high")
assert payload["output_config"]["effort"] == "high"
def test_reasoning_effort_does_not_warn() -> None:
"""Test that the non-deprecated `reasoning_effort` field never warns."""
model = ChatAnthropic(model="claude-opus-4-5-20251101", reasoning_effort="high")
with warnings.catch_warnings():
warnings.simplefilter("error")
payload = model._get_request_payload("Test query")
assert payload["output_config"]["effort"] == "high"
def test_effort_in_output_config() -> None:
"""Test that effort can be specified in `output_config`."""
# Test valid effort in output_config
@@ -2926,7 +2952,6 @@ def test_effort_priority() -> None:
output_config={"effort": "low"},
)
# Top-level effort should take precedence in the payload
payload = model._get_request_payload("Test query")
assert payload["output_config"]["effort"] == "high"
@@ -2942,6 +2967,160 @@ def test_output_config_without_effort() -> None:
assert payload["output_config"] == {"some_future_param": "value"}
def test_reasoning_effort_parameter_validation() -> None:
"""Test that `reasoning_effort` is validated the same as `effort`."""
model = ChatAnthropic(model="claude-opus-4-5-20251101", reasoning_effort="high")
assert model.reasoning_effort == "high"
with pytest.raises(ValidationError, match="Input should be"):
ChatAnthropic(model="claude-opus-4-5-20251101", reasoning_effort="invalid") # type: ignore[arg-type]
def test_reasoning_effort_in_output_config_payload() -> None:
"""Test that a construction-time `reasoning_effort` reaches `output_config`."""
model = ChatAnthropic(model="claude-opus-4-5-20251101", reasoning_effort="medium")
payload = model._get_request_payload("Test query")
assert payload["output_config"]["effort"] == "medium"
def test_reasoning_effort_as_call_time_kwarg() -> None:
"""Test that `reasoning_effort` also works as a call-time keyword argument.
This is the standard `reasoning_effort` param shared across chat model
integrations, so it must work via `model.invoke(..., reasoning_effort=...)`
without requiring it to be set on the model instance.
"""
model = ChatAnthropic(model="claude-opus-4-5-20251101")
payload = model._get_request_payload("Test query", reasoning_effort="low")
assert payload["output_config"]["effort"] == "low"
# Never leaks through as a stray top-level key -- not a real Anthropic field.
assert "reasoning_effort" not in payload
def test_reasoning_effort_call_time_kwarg_overrides_construction_time() -> None:
"""Test that a call-time `reasoning_effort` overrides the instance default."""
model = ChatAnthropic(model="claude-opus-4-5-20251101", reasoning_effort="low")
payload = model._get_request_payload("Test query", reasoning_effort="high")
assert payload["output_config"]["effort"] == "high"
def test_reasoning_effort_yields_to_effort() -> None:
"""Test that `effort` still takes precedence over `reasoning_effort`.
`effort` is a `Field(alias="effort")` on `reasoning_effort`, and Pydantic's
alias-resolution precedence has the alias win when both are supplied.
"""
model = ChatAnthropic(
model="claude-opus-4-5-20251101",
effort="high",
reasoning_effort="low",
)
payload = model._get_request_payload("Test query")
assert payload["output_config"]["effort"] == "high"
def test_reasoning_effort_defaults_adaptive_thinking() -> None:
"""Test that `reasoning_effort` also defaults `thinking` to adaptive.
Mirrors the reasoning-effort behavior previously implemented client-side
(pairing the effort level with adaptive thinking), so the model actually
reasons harder instead of being told a preference with no active
reasoning mode to apply it to. Only models whose profile advertises
`xhigh` (Opus 4.7+, Sonnet 5) accept this `thinking` shape.
"""
model = ChatAnthropic(model="claude-opus-4-7", reasoning_effort="high")
payload = model._get_request_payload("Test query")
assert payload["thinking"] == {"type": "adaptive", "display": "summarized"}
assert payload["output_config"]["effort"] == "high"
def test_reasoning_effort_as_call_time_kwarg_defaults_adaptive_thinking() -> None:
"""Test that a call-time `reasoning_effort` also defaults `thinking`."""
model = ChatAnthropic(model="claude-opus-4-7")
payload = model._get_request_payload("Test query", reasoning_effort="high")
assert payload["thinking"] == {"type": "adaptive", "display": "summarized"}
def test_reasoning_effort_older_model_does_not_default_thinking() -> None:
"""Older models must not get an adaptive `thinking` default.
Regression test: Opus 4.5/4.6 support `reasoning_effort` but reject the
adaptive+summarized `thinking` shape with a 400 from the real API
("adaptive thinking is not supported on this model"). Only models whose
profile declares `xhigh` support should get the `thinking` default.
"""
model = ChatAnthropic(model="claude-opus-4-5-20251101", reasoning_effort="high")
payload = model._get_request_payload("Test query")
assert "thinking" not in payload
assert payload["output_config"]["effort"] == "high"
def test_reasoning_effort_preserves_explicit_construction_time_thinking() -> None:
"""Test that an explicit `thinking` field is not clobbered by `reasoning_effort`."""
model = ChatAnthropic(
model="claude-opus-4-5-20251101",
reasoning_effort="high",
thinking={"type": "enabled", "budget_tokens": 10_000},
)
payload = model._get_request_payload("Test query")
assert payload["thinking"] == {"type": "enabled", "budget_tokens": 10_000}
def test_reasoning_effort_preserves_explicit_call_time_thinking() -> None:
"""Test that a call-time `thinking` kwarg is not clobbered by `reasoning_effort`."""
model = ChatAnthropic(model="claude-opus-4-5-20251101")
payload = model._get_request_payload(
"Test query",
reasoning_effort="high",
thinking={"type": "disabled"},
)
assert payload["thinking"] == {"type": "disabled"}
def test_effort_also_defaults_adaptive_thinking() -> None:
"""Test that `effort` composes with the adaptive-thinking default too.
`effort` is a pure alias for `reasoning_effort` (`Field(alias="effort")`),
so they behave identically -- including triggering the adaptive `thinking`
default on `xhigh`-capable models. There's no separate "narrower" behavior
for `effort` anymore, since it's not a separate value.
"""
model = ChatAnthropic(model="claude-opus-4-7", effort="high")
payload = model._get_request_payload("Test query")
assert payload["thinking"] == {"type": "adaptive", "display": "summarized"}
def test_effort_older_model_does_not_default_thinking() -> None:
"""Test that `effort` on a non-`xhigh` model still doesn't default `thinking`.
Gated on model support (via the model's profile), not on which field name
was used to set the effort level.
"""
model = ChatAnthropic(model="claude-opus-4-5-20251101", effort="high")
payload = model._get_request_payload("Test query")
assert "thinking" not in payload
def test_extras_with_defer_loading() -> None:
"""Test that extras with `defer_loading` are merged into tool definitions."""
@@ -895,6 +895,15 @@ class ChatFireworks(BaseChatModel):
!!! version-added "Added in `langchain-fireworks` 1.3.0"
"""
reasoning_effort: str | None = None
"""Reasoning effort.
Forwarded as the `reasoning_effort` request field. Supported values vary by
model; see the model's `profile.reasoning_effort_levels`.
Can also be passed at call time, e.g.
`model.invoke(..., reasoning_effort="high")`.
"""
model_config = ConfigDict(
populate_by_name=True,
@@ -994,6 +1003,8 @@ class ChatFireworks(BaseChatModel):
params["max_tokens"] = self.max_tokens
if self.service_tier is not None:
params["service_tier"] = self.service_tier
if self.reasoning_effort is not None:
params["reasoning_effort"] = self.reasoning_effort
return params
def _get_ls_params(
@@ -59,6 +59,15 @@ _PROFILES: dict[str, dict[str, Any]] = {
"attachment": False,
"temperature": True,
"tool_call_streaming": True,
"reasoning_effort_levels": [
"none",
"low",
"medium",
"high",
"xhigh",
"max",
],
"reasoning_effort_default": "high",
},
"accounts/fireworks/models/glm-5p1": {
"name": "GLM 5.1",
@@ -80,6 +89,11 @@ _PROFILES: dict[str, dict[str, Any]] = {
"attachment": False,
"temperature": True,
"tool_call_streaming": True,
"reasoning_effort_levels": [
"none",
"high",
"max",
],
},
"accounts/fireworks/models/glm-5p2": {
"name": "GLM 5.2",
@@ -101,6 +115,12 @@ _PROFILES: dict[str, dict[str, Any]] = {
"attachment": False,
"temperature": True,
"tool_call_streaming": True,
"reasoning_effort_levels": [
"none",
"high",
"max",
],
"reasoning_effort_default": "max",
},
"accounts/fireworks/models/gpt-oss-120b": {
"name": "GPT OSS 120B",
@@ -164,6 +184,11 @@ _PROFILES: dict[str, dict[str, Any]] = {
"attachment": True,
"temperature": True,
"tool_call_streaming": True,
"reasoning_effort_levels": [
"low",
"medium",
"high",
],
},
"accounts/fireworks/models/kimi-k2p7-code": {
"name": "Kimi K2.7 Code",
@@ -185,6 +210,11 @@ _PROFILES: dict[str, dict[str, Any]] = {
"attachment": True,
"temperature": True,
"tool_call_streaming": True,
"reasoning_effort_levels": [
"low",
"medium",
"high",
],
},
"accounts/fireworks/models/minimax-m2p7": {
"name": "MiniMax-M2.7",
@@ -269,6 +299,11 @@ _PROFILES: dict[str, dict[str, Any]] = {
"attachment": False,
"temperature": True,
"tool_call_streaming": True,
"reasoning_effort_levels": [
"none",
"high",
"max",
],
},
"accounts/fireworks/routers/glm-5p2-fast": {
"name": "GLM 5.2 Fast",
@@ -290,6 +325,12 @@ _PROFILES: dict[str, dict[str, Any]] = {
"attachment": False,
"temperature": True,
"tool_call_streaming": True,
"reasoning_effort_levels": [
"none",
"high",
"max",
],
"reasoning_effort_default": "max",
},
"accounts/fireworks/routers/kimi-k2p6-fast": {
"name": "Kimi K2.6 Fast",
@@ -311,6 +352,11 @@ _PROFILES: dict[str, dict[str, Any]] = {
"attachment": True,
"temperature": True,
"tool_call_streaming": True,
"reasoning_effort_levels": [
"low",
"medium",
"high",
],
},
"accounts/fireworks/routers/kimi-k2p6-turbo": {
"name": "Kimi K2.6 Turbo",
@@ -332,6 +378,11 @@ _PROFILES: dict[str, dict[str, Any]] = {
"attachment": True,
"temperature": True,
"tool_call_streaming": True,
"reasoning_effort_levels": [
"low",
"medium",
"high",
],
},
"accounts/fireworks/routers/kimi-k2p7-code-fast": {
"name": "Kimi K2.7 Code Fast",
@@ -353,5 +404,10 @@ _PROFILES: dict[str, dict[str, Any]] = {
"attachment": True,
"temperature": True,
"tool_call_streaming": True,
"reasoning_effort_levels": [
"low",
"medium",
"high",
],
},
}
@@ -2,3 +2,36 @@ provider = "fireworks-ai"
[overrides]
tool_call_streaming = true
[overrides."accounts/fireworks/models/deepseek-v4-pro"]
reasoning_effort_levels = ["none", "low", "medium", "high", "xhigh", "max"]
reasoning_effort_default = "high"
[overrides."accounts/fireworks/models/glm-5p1"]
reasoning_effort_levels = ["none", "high", "max"]
[overrides."accounts/fireworks/models/glm-5p2"]
reasoning_effort_levels = ["none", "high", "max"]
reasoning_effort_default = "max"
[overrides."accounts/fireworks/models/kimi-k2p6"]
reasoning_effort_levels = ["low", "medium", "high"]
[overrides."accounts/fireworks/models/kimi-k2p7-code"]
reasoning_effort_levels = ["low", "medium", "high"]
[overrides."accounts/fireworks/routers/glm-5p1-fast"]
reasoning_effort_levels = ["none", "high", "max"]
[overrides."accounts/fireworks/routers/glm-5p2-fast"]
reasoning_effort_levels = ["none", "high", "max"]
reasoning_effort_default = "max"
[overrides."accounts/fireworks/routers/kimi-k2p6-fast"]
reasoning_effort_levels = ["low", "medium", "high"]
[overrides."accounts/fireworks/routers/kimi-k2p6-turbo"]
reasoning_effort_levels = ["low", "medium", "high"]
[overrides."accounts/fireworks/routers/kimi-k2p7-code-fast"]
reasoning_effort_levels = ["low", "medium", "high"]
@@ -178,3 +178,33 @@ def test_structured_output_json_schema(schema_type: str) -> None:
validation_function(chunk)
chunks.append(chunk)
assert chunk
def test_reasoning_effort_parameter() -> None:
"""Test that the standard `reasoning_effort` parameter is accepted by the API."""
llm = ChatFireworks(
model="accounts/fireworks/models/kimi-k2p6",
reasoning_effort="high",
rate_limiter=rate_limiter,
)
result = llm.invoke("Say hello in one sentence")
assert isinstance(result.content, str)
assert len(result.content) > 0
assert result.usage_metadata is not None
assert result.usage_metadata["input_tokens"] > 0
assert result.usage_metadata["output_tokens"] > 0
def test_reasoning_effort_call_time_kwarg() -> None:
"""Test that `reasoning_effort` is accepted as a call-time kwarg."""
llm = ChatFireworks(
model="accounts/fireworks/models/kimi-k2p6", rate_limiter=rate_limiter
)
result = llm.invoke("Say hello in one sentence", reasoning_effort="high")
assert isinstance(result.content, str)
assert len(result.content) > 0
assert result.usage_metadata is not None
@@ -1524,6 +1524,90 @@ class TestStreamUsage:
}
class TestReasoningEffort:
"""Tests for the `reasoning_effort` field plumbing."""
def test_reasoning_effort_omitted_by_default(self) -> None:
model = _make_model()
assert "reasoning_effort" not in model._default_params
def test_reasoning_effort_in_default_params_when_set(self) -> None:
model = _make_model(reasoning_effort="high")
assert model._default_params["reasoning_effort"] == "high"
def test_reasoning_effort_passed_to_client_when_set(self) -> None:
model = _make_model(reasoning_effort="high")
model.client = MagicMock()
model.client.create.return_value = {
"choices": [
{
"message": {"role": "assistant", "content": "hi"},
"finish_reason": "stop",
}
],
"usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2},
}
model.invoke("Hello")
call_kwargs = model.client.create.call_args[1]
assert call_kwargs["reasoning_effort"] == "high"
def test_reasoning_effort_not_passed_when_unset(self) -> None:
model = _make_model()
model.client = MagicMock()
model.client.create.return_value = {
"choices": [
{
"message": {"role": "assistant", "content": "hi"},
"finish_reason": "stop",
}
],
"usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2},
}
model.invoke("Hello")
call_kwargs = model.client.create.call_args[1]
assert "reasoning_effort" not in call_kwargs
def test_reasoning_effort_as_call_time_kwarg(self) -> None:
"""`reasoning_effort` also works as a call-time keyword argument.
This is the standard `reasoning_effort` param shared across chat model
integrations, so it must work via `model.invoke(..., reasoning_effort=...)`
without requiring it to be set on the model instance.
"""
model = _make_model()
model.client = MagicMock()
model.client.create.return_value = {
"choices": [
{
"message": {"role": "assistant", "content": "hi"},
"finish_reason": "stop",
}
],
"usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2},
}
model.invoke("Hello", reasoning_effort="low")
call_kwargs = model.client.create.call_args[1]
assert call_kwargs["reasoning_effort"] == "low"
def test_reasoning_effort_call_time_kwarg_overrides_construction_time(
self,
) -> None:
model = _make_model(reasoning_effort="low")
model.client = MagicMock()
model.client.create.return_value = {
"choices": [
{
"message": {"role": "assistant", "content": "hi"},
"finish_reason": "stop",
}
],
"usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2},
}
model.invoke("Hello", reasoning_effort="high")
call_kwargs = model.client.create.call_args[1]
assert call_kwargs["reasoning_effort"] == "high"
class TestServiceTier:
"""Tests for the `service_tier` field plumbing."""
@@ -365,6 +365,13 @@ _PROFILES: dict[str, dict[str, Any]] = {
"image_tool_message": True,
"tool_choice": True,
"tool_call_streaming": True,
"reasoning_effort_levels": [
"none",
"low",
"medium",
"high",
"xhigh",
],
},
"gpt-5-chat-latest": {
"name": "GPT-5 Chat (latest)",
@@ -392,6 +399,13 @@ _PROFILES: dict[str, dict[str, Any]] = {
"image_tool_message": True,
"tool_choice": True,
"tool_call_streaming": True,
"reasoning_effort_levels": [
"none",
"low",
"medium",
"high",
"xhigh",
],
},
"gpt-5-codex": {
"name": "GPT-5-Codex",
@@ -419,6 +433,13 @@ _PROFILES: dict[str, dict[str, Any]] = {
"image_tool_message": True,
"tool_choice": True,
"tool_call_streaming": True,
"reasoning_effort_levels": [
"none",
"low",
"medium",
"high",
"xhigh",
],
},
"gpt-5-mini": {
"name": "GPT-5 Mini",
@@ -446,6 +467,13 @@ _PROFILES: dict[str, dict[str, Any]] = {
"image_tool_message": True,
"tool_choice": True,
"tool_call_streaming": True,
"reasoning_effort_levels": [
"none",
"low",
"medium",
"high",
"xhigh",
],
},
"gpt-5-nano": {
"name": "GPT-5 Nano",
@@ -473,6 +501,13 @@ _PROFILES: dict[str, dict[str, Any]] = {
"image_tool_message": True,
"tool_choice": True,
"tool_call_streaming": True,
"reasoning_effort_levels": [
"none",
"low",
"medium",
"high",
"xhigh",
],
},
"gpt-5-pro": {
"name": "GPT-5 Pro",
@@ -500,6 +535,13 @@ _PROFILES: dict[str, dict[str, Any]] = {
"image_tool_message": True,
"tool_choice": True,
"tool_call_streaming": True,
"reasoning_effort_levels": [
"none",
"low",
"medium",
"high",
"xhigh",
],
},
"gpt-5.1": {
"name": "GPT-5.1",
@@ -527,6 +569,13 @@ _PROFILES: dict[str, dict[str, Any]] = {
"image_tool_message": True,
"tool_choice": True,
"tool_call_streaming": True,
"reasoning_effort_levels": [
"none",
"low",
"medium",
"high",
"xhigh",
],
},
"gpt-5.1-chat-latest": {
"name": "GPT-5.1 Chat",
@@ -554,6 +603,13 @@ _PROFILES: dict[str, dict[str, Any]] = {
"image_tool_message": True,
"tool_choice": True,
"tool_call_streaming": True,
"reasoning_effort_levels": [
"none",
"low",
"medium",
"high",
"xhigh",
],
},
"gpt-5.1-codex": {
"name": "GPT-5.1 Codex",
@@ -581,6 +637,13 @@ _PROFILES: dict[str, dict[str, Any]] = {
"image_tool_message": True,
"tool_choice": True,
"tool_call_streaming": True,
"reasoning_effort_levels": [
"none",
"low",
"medium",
"high",
"xhigh",
],
},
"gpt-5.1-codex-max": {
"name": "GPT-5.1 Codex Max",
@@ -608,6 +671,13 @@ _PROFILES: dict[str, dict[str, Any]] = {
"image_tool_message": True,
"tool_choice": True,
"tool_call_streaming": True,
"reasoning_effort_levels": [
"none",
"low",
"medium",
"high",
"xhigh",
],
},
"gpt-5.1-codex-mini": {
"name": "GPT-5.1 Codex mini",
@@ -635,6 +705,13 @@ _PROFILES: dict[str, dict[str, Any]] = {
"image_tool_message": True,
"tool_choice": True,
"tool_call_streaming": True,
"reasoning_effort_levels": [
"none",
"low",
"medium",
"high",
"xhigh",
],
},
"gpt-5.2": {
"name": "GPT-5.2",
@@ -662,6 +739,13 @@ _PROFILES: dict[str, dict[str, Any]] = {
"image_tool_message": True,
"tool_choice": True,
"tool_call_streaming": True,
"reasoning_effort_levels": [
"none",
"low",
"medium",
"high",
"xhigh",
],
},
"gpt-5.2-chat-latest": {
"name": "GPT-5.2 Chat",
@@ -689,6 +773,13 @@ _PROFILES: dict[str, dict[str, Any]] = {
"image_tool_message": True,
"tool_choice": True,
"tool_call_streaming": True,
"reasoning_effort_levels": [
"none",
"low",
"medium",
"high",
"xhigh",
],
},
"gpt-5.2-codex": {
"name": "GPT-5.2 Codex",
@@ -716,6 +807,13 @@ _PROFILES: dict[str, dict[str, Any]] = {
"image_tool_message": True,
"tool_choice": True,
"tool_call_streaming": True,
"reasoning_effort_levels": [
"none",
"low",
"medium",
"high",
"xhigh",
],
},
"gpt-5.2-pro": {
"name": "GPT-5.2 Pro",
@@ -743,6 +841,13 @@ _PROFILES: dict[str, dict[str, Any]] = {
"image_tool_message": True,
"tool_choice": True,
"tool_call_streaming": True,
"reasoning_effort_levels": [
"none",
"low",
"medium",
"high",
"xhigh",
],
},
"gpt-5.3-chat-latest": {
"name": "GPT-5.3 Chat (latest)",
@@ -770,6 +875,13 @@ _PROFILES: dict[str, dict[str, Any]] = {
"image_tool_message": True,
"tool_choice": True,
"tool_call_streaming": True,
"reasoning_effort_levels": [
"none",
"low",
"medium",
"high",
"xhigh",
],
},
"gpt-5.3-codex": {
"name": "GPT-5.3 Codex",
@@ -797,6 +909,13 @@ _PROFILES: dict[str, dict[str, Any]] = {
"image_tool_message": True,
"tool_choice": True,
"tool_call_streaming": True,
"reasoning_effort_levels": [
"none",
"low",
"medium",
"high",
"xhigh",
],
},
"gpt-5.3-codex-spark": {
"name": "GPT-5.3 Codex Spark",
@@ -824,6 +943,13 @@ _PROFILES: dict[str, dict[str, Any]] = {
"image_tool_message": True,
"tool_choice": True,
"tool_call_streaming": True,
"reasoning_effort_levels": [
"none",
"low",
"medium",
"high",
"xhigh",
],
},
"gpt-5.4": {
"name": "GPT-5.4",
@@ -851,6 +977,13 @@ _PROFILES: dict[str, dict[str, Any]] = {
"image_tool_message": True,
"tool_choice": True,
"tool_call_streaming": True,
"reasoning_effort_levels": [
"none",
"low",
"medium",
"high",
"xhigh",
],
},
"gpt-5.4-mini": {
"name": "GPT-5.4 mini",
@@ -878,6 +1011,13 @@ _PROFILES: dict[str, dict[str, Any]] = {
"image_tool_message": True,
"tool_choice": True,
"tool_call_streaming": True,
"reasoning_effort_levels": [
"none",
"low",
"medium",
"high",
"xhigh",
],
},
"gpt-5.4-nano": {
"name": "GPT-5.4 nano",
@@ -905,6 +1045,13 @@ _PROFILES: dict[str, dict[str, Any]] = {
"image_tool_message": True,
"tool_choice": True,
"tool_call_streaming": True,
"reasoning_effort_levels": [
"none",
"low",
"medium",
"high",
"xhigh",
],
},
"gpt-5.4-pro": {
"name": "GPT-5.4 Pro",
@@ -932,6 +1079,13 @@ _PROFILES: dict[str, dict[str, Any]] = {
"image_tool_message": True,
"tool_choice": True,
"tool_call_streaming": True,
"reasoning_effort_levels": [
"none",
"low",
"medium",
"high",
"xhigh",
],
},
"gpt-5.5": {
"name": "GPT-5.5",
@@ -959,6 +1113,14 @@ _PROFILES: dict[str, dict[str, Any]] = {
"image_tool_message": True,
"tool_choice": True,
"tool_call_streaming": True,
"reasoning_effort_levels": [
"none",
"low",
"medium",
"high",
"xhigh",
],
"reasoning_effort_default": "medium",
},
"gpt-5.5-pro": {
"name": "GPT-5.5 Pro",
@@ -986,6 +1148,14 @@ _PROFILES: dict[str, dict[str, Any]] = {
"image_tool_message": True,
"tool_choice": True,
"tool_call_streaming": True,
"reasoning_effort_levels": [
"none",
"low",
"medium",
"high",
"xhigh",
],
"reasoning_effort_default": "medium",
},
"gpt-5.6": {
"name": "GPT-5.6",
@@ -1013,6 +1183,15 @@ _PROFILES: dict[str, dict[str, Any]] = {
"image_tool_message": True,
"tool_choice": True,
"tool_call_streaming": True,
"reasoning_effort_levels": [
"none",
"low",
"medium",
"high",
"xhigh",
"max",
],
"reasoning_effort_default": "medium",
},
"gpt-5.6-luna": {
"name": "GPT-5.6 Luna",
@@ -1040,6 +1219,15 @@ _PROFILES: dict[str, dict[str, Any]] = {
"image_tool_message": True,
"tool_choice": True,
"tool_call_streaming": True,
"reasoning_effort_levels": [
"none",
"low",
"medium",
"high",
"xhigh",
"max",
],
"reasoning_effort_default": "medium",
},
"gpt-5.6-sol": {
"name": "GPT-5.6 Sol",
@@ -1067,6 +1255,15 @@ _PROFILES: dict[str, dict[str, Any]] = {
"image_tool_message": True,
"tool_choice": True,
"tool_call_streaming": True,
"reasoning_effort_levels": [
"none",
"low",
"medium",
"high",
"xhigh",
"max",
],
"reasoning_effort_default": "medium",
},
"gpt-5.6-terra": {
"name": "GPT-5.6 Terra",
@@ -1094,6 +1291,15 @@ _PROFILES: dict[str, dict[str, Any]] = {
"image_tool_message": True,
"tool_choice": True,
"tool_call_streaming": True,
"reasoning_effort_levels": [
"none",
"low",
"medium",
"high",
"xhigh",
"max",
],
"reasoning_effort_default": "medium",
},
"gpt-image-1": {
"name": "gpt-image-1",
@@ -16,42 +16,104 @@ image_tool_message = false
[overrides."gpt-5.1-codex"]
max_input_tokens = 272000
reasoning_effort_levels = ["none", "low", "medium", "high", "xhigh"]
[overrides."gpt-5.2-pro"]
max_input_tokens = 272000
reasoning_effort_levels = ["none", "low", "medium", "high", "xhigh"]
[overrides."gpt-5.1-codex-mini"]
max_input_tokens = 272000
reasoning_effort_levels = ["none", "low", "medium", "high", "xhigh"]
[overrides."gpt-5.2-chat-latest"]
max_input_tokens = 272000
reasoning_effort_levels = ["none", "low", "medium", "high", "xhigh"]
[overrides."gpt-5.1"]
max_input_tokens = 272000
reasoning_effort_levels = ["none", "low", "medium", "high", "xhigh"]
[overrides."gpt-5-nano"]
max_input_tokens = 272000
reasoning_effort_levels = ["none", "low", "medium", "high", "xhigh"]
[overrides."gpt-5-codex"]
max_input_tokens = 272000
reasoning_effort_levels = ["none", "low", "medium", "high", "xhigh"]
[overrides."gpt-5-mini"]
max_input_tokens = 272000
reasoning_effort_levels = ["none", "low", "medium", "high", "xhigh"]
[overrides."gpt-5.1-codex-max"]
max_input_tokens = 272000
reasoning_effort_levels = ["none", "low", "medium", "high", "xhigh"]
[overrides."gpt-5-chat-latest"]
max_input_tokens = 272000
reasoning_effort_levels = ["none", "low", "medium", "high", "xhigh"]
[overrides."gpt-5"]
max_input_tokens = 272000
reasoning_effort_levels = ["none", "low", "medium", "high", "xhigh"]
[overrides."gpt-5-pro"]
max_input_tokens = 272000
reasoning_effort_levels = ["none", "low", "medium", "high", "xhigh"]
[overrides."gpt-5.2"]
max_input_tokens = 272000
reasoning_effort_levels = ["none", "low", "medium", "high", "xhigh"]
[overrides."gpt-5.1-chat-latest"]
max_input_tokens = 272000
reasoning_effort_levels = ["none", "low", "medium", "high", "xhigh"]
[overrides."gpt-5.2-codex"]
reasoning_effort_levels = ["none", "low", "medium", "high", "xhigh"]
[overrides."gpt-5.3-chat-latest"]
reasoning_effort_levels = ["none", "low", "medium", "high", "xhigh"]
[overrides."gpt-5.3-codex"]
reasoning_effort_levels = ["none", "low", "medium", "high", "xhigh"]
[overrides."gpt-5.3-codex-spark"]
reasoning_effort_levels = ["none", "low", "medium", "high", "xhigh"]
[overrides."gpt-5.4"]
reasoning_effort_levels = ["none", "low", "medium", "high", "xhigh"]
[overrides."gpt-5.4-mini"]
reasoning_effort_levels = ["none", "low", "medium", "high", "xhigh"]
[overrides."gpt-5.4-nano"]
reasoning_effort_levels = ["none", "low", "medium", "high", "xhigh"]
[overrides."gpt-5.4-pro"]
reasoning_effort_levels = ["none", "low", "medium", "high", "xhigh"]
[overrides."gpt-5.5"]
reasoning_effort_levels = ["none", "low", "medium", "high", "xhigh"]
reasoning_effort_default = "medium"
[overrides."gpt-5.5-pro"]
reasoning_effort_levels = ["none", "low", "medium", "high", "xhigh"]
reasoning_effort_default = "medium"
[overrides."gpt-5.6"]
reasoning_effort_levels = ["none", "low", "medium", "high", "xhigh", "max"]
reasoning_effort_default = "medium"
[overrides."gpt-5.6-luna"]
reasoning_effort_levels = ["none", "low", "medium", "high", "xhigh", "max"]
reasoning_effort_default = "medium"
[overrides."gpt-5.6-sol"]
reasoning_effort_levels = ["none", "low", "medium", "high", "xhigh", "max"]
reasoning_effort_default = "medium"
[overrides."gpt-5.6-terra"]
reasoning_effort_levels = ["none", "low", "medium", "high", "xhigh", "max"]
reasoning_effort_default = "medium"
@@ -1072,6 +1072,30 @@ def test_reasoning_model_stream_default_works() -> None:
assert len(result) > 0
def test_reasoning_effort_parameter() -> None:
"""Test that the standard `reasoning_effort` parameter is accepted by the API."""
llm = ChatOpenAI(model="gpt-5-nano", reasoning_effort="low")
result = llm.invoke("Say hello in one sentence")
assert isinstance(result.content, str)
assert len(result.content) > 0
assert result.usage_metadata is not None
assert result.usage_metadata["input_tokens"] > 0
assert result.usage_metadata["output_tokens"] > 0
def test_reasoning_effort_call_time_kwarg() -> None:
"""Test that `reasoning_effort` is accepted as a call-time kwarg."""
llm = ChatOpenAI(model="gpt-5-nano")
result = llm.invoke("Say hello in one sentence", reasoning_effort="low")
assert isinstance(result.content, str)
assert len(result.content) > 0
assert result.usage_metadata is not None
@pytest.mark.flaky(retries=3, delay=1)
def test_multi_party_conversation() -> None:
llm = ChatOpenAI(model="gpt-5-nano")
@@ -1547,7 +1547,7 @@ def test_minimal_reasoning_effort_payload(
# When using responses API, reasoning_effort becomes reasoning.effort
if use_responses_api:
assert "reasoning" in payload
assert payload["reasoning"]["effort"] == "minimal"
assert payload["reasoning"] == {"effort": "minimal"}
# For responses API, tokens param becomes max_output_tokens
assert payload["max_output_tokens"] == 100
else:
@@ -4616,6 +4616,39 @@ def test_namespace_passthrough() -> None:
assert {"type": "tool_search"} in payload["tools"]
def test_reasoning_effort_responses_api_maps_to_effort_only() -> None:
"""Test `reasoning_effort` maps to `reasoning.effort` alone, no `summary`.
`summary` is a separate concern, configured via the `reasoning` param
directly (e.g. `reasoning={"effort": "high", "summary": "auto"}`) rather
than implied by `reasoning_effort`.
"""
from langchain_openai.chat_models.base import _construct_responses_api_payload
payload = _construct_responses_api_payload([], {"reasoning_effort": "high"})
assert payload["reasoning"] == {"effort": "high"}
assert "reasoning_effort" not in payload
def test_reasoning_effort_responses_api_preserves_existing_reasoning() -> None:
"""Test that an already-present `reasoning` dict is not overwritten."""
from langchain_openai.chat_models.base import _construct_responses_api_payload
payload = _construct_responses_api_payload(
[],
{
"reasoning_effort": "high",
"reasoning": {"effort": "low", "summary": "concise"},
},
)
assert payload["reasoning"] == {"effort": "low", "summary": "concise"}
# The unused `reasoning_effort` kwarg is left untouched in this case, since
# the guard requires `"reasoning" not in payload` before popping it.
assert payload["reasoning_effort"] == "high"
def test_defer_loading_in_responses_api_payload() -> None:
"""Test that defer_loading is preserved in Responses API tool format."""
from langchain_openai.chat_models.base import _construct_responses_api_payload
@@ -586,6 +586,16 @@ class ChatXAI(BaseChatOpenAI): # type: ignore[override]
)
if rejects_stop:
payload.pop("stop", None)
# xAI expects `reasoning_effort` in `extra_body`, not as a top-level field.
# Move it there so it reaches the API.
reasoning_effort = payload.pop("reasoning_effort", None)
if reasoning_effort is not None:
extra_body = payload.get("extra_body")
if not isinstance(extra_body, dict):
extra_body = {}
payload["extra_body"] = {**extra_body, "reasoning_effort": reasoning_effort}
return payload
def _stream(self, *args: Any, **kwargs: Any) -> Iterator[ChatGenerationChunk]:
@@ -107,6 +107,13 @@ _PROFILES: dict[str, dict[str, Any]] = {
"attachment": True,
"temperature": True,
"tool_call_streaming": True,
"reasoning_effort_levels": [
"none",
"low",
"medium",
"high",
],
"reasoning_effort_default": "low",
},
"grok-4.5": {
"name": "Grok 4.5",
@@ -2,3 +2,7 @@ provider = "xai"
[overrides]
tool_call_streaming = true
[overrides."grok-4.3"]
reasoning_effort_levels = ["none", "low", "medium", "high"]
reasoning_effort_default = "low"
@@ -97,6 +97,17 @@ def test_reasoning(output_version: Literal["", "v1"]) -> None:
assert followup_2.additional_kwargs["reasoning_content"]
def test_reasoning_effort_call_time_kwarg() -> None:
"""Test that `reasoning_effort` is accepted as a call-time kwarg."""
chat_model = ChatXAI(model="grok-3-mini", temperature=0)
response = chat_model.invoke("What is 3^3?", reasoning_effort="low")
assert response.content
assert response.additional_kwargs["reasoning_content"]
assert response.usage_metadata is not None
def test_web_search() -> None:
llm = ChatXAI(model=MODEL_NAME, temperature=0).bind_tools([{"type": "web_search"}])
@@ -130,6 +130,67 @@ def test_non_reasoning_model_payload_keeps_stop() -> None:
assert payload["stop"] == ["END"]
def test_reasoning_effort_moved_to_extra_body() -> None:
"""`reasoning_effort` (inherited from `BaseChatOpenAI`) must reach xAI's
API via `extra_body`, since xAI does not accept it as a top-level field.
"""
llm = ChatXAI(
model="grok-3-mini",
api_key=SecretStr("test-api-key"),
reasoning_effort="high",
)
payload = llm._get_request_payload("hello")
assert "reasoning_effort" not in payload
assert payload["extra_body"]["reasoning_effort"] == "high"
def test_reasoning_effort_as_call_time_kwarg() -> None:
"""`reasoning_effort` also works as a call-time keyword argument.
This is the standard `reasoning_effort` param shared across chat model
integrations, so it must work via `model.invoke(..., reasoning_effort=...)`
without requiring it to be set on the model instance.
"""
llm = ChatXAI(model="grok-3-mini", api_key=SecretStr("test-api-key"))
payload = llm._get_request_payload("hello", reasoning_effort="low")
assert "reasoning_effort" not in payload
assert payload["extra_body"]["reasoning_effort"] == "low"
def test_reasoning_effort_preserves_existing_extra_body() -> None:
"""Moving `reasoning_effort` into `extra_body` must not drop sibling keys."""
llm = ChatXAI(
model="grok-3-mini",
api_key=SecretStr("test-api-key"),
reasoning_effort="high",
extra_body={"some_other_field": "value"},
)
payload = llm._get_request_payload("hello")
assert payload["extra_body"] == {
"some_other_field": "value",
"reasoning_effort": "high",
}
def test_no_reasoning_effort_leaves_extra_body_untouched() -> None:
llm = ChatXAI(
model="grok-3-mini",
api_key=SecretStr("test-api-key"),
extra_body={"some_other_field": "value"},
)
payload = llm._get_request_payload("hello")
assert payload["extra_body"] == {"some_other_field": "value"}
assert "reasoning_effort" not in payload
def test_function_dict_to_message_function_message() -> None:
content = json.dumps({"result": "Example #1"})
name = "test_function"