mirror of
https://github.com/langchain-ai/langchain.git
synced 2026-10-05 17:35:28 +03:00
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:
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"
|
||||
|
||||
Reference in new issue
Block a user