mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-09-14 16:08:41 +00:00
fix(models): stop reasoning_effort from reaching the constructor twice (#5403)
* fix(models): stop reasoning_effort from reaching the constructor twice The regular lead-agent build forwards reasoning_effort to create_chat_model even when neither the request nor the custom agent chose one. The factory spread that kwarg next to the profile settings, so any profile that also yielded reasoning_effort -- a top-level value, when_thinking_enabled, when_thinking_disabled, or the minimal effort the extra_body.thinking disable path injects -- made the constructor raise "got multiple values for keyword argument 'reasoning_effort'", and the lead agent could not be built for that model. #2017 moved the factory-injected value out of kwargs but left the caller-supplied one. The requested effort now leaves kwargs once and layers like model_overrides: a non-None value replaces the profile value, None keeps it, and the thinking transforms applied afterwards still decide the final value. Codex keeps resolving the requested value itself, so a level it does not accept and a profile without effort support still fall back to medium. * docs(changelog): reference #5403 in the reasoning_effort collision fix entry
This commit is contained in:
parent
6f81daefff
commit
2814bd5d49
11
CHANGELOG.md
11
CHANGELOG.md
@ -582,6 +582,16 @@ This section accumulates work toward the **2.1.0** milestone
|
||||
|
||||
### Fixed
|
||||
|
||||
- **models:** Stop the lead agent from failing to build whenever a model with
|
||||
`supports_reasoning_effort: true` also gets a `reasoning_effort` from its
|
||||
profile — at the top level, in `when_thinking_enabled` or
|
||||
`when_thinking_disabled`, or from the `extra_body.thinking` disable path. The
|
||||
regular lead-agent build forwards the requested effort even when unset, so
|
||||
the key reached the provider constructor twice and raised `TypeError: got
|
||||
multiple values for keyword argument 'reasoning_effort'`. The requested value now
|
||||
layers like per-agent `model_settings`: it replaces a top-level profile
|
||||
value, an unset request keeps that value, and the thinking-mode settings still
|
||||
decide the final one. Codex keeps its own level check. ([#5403])
|
||||
- **runtime:** Stop a keyed run retry from failing with 500 on the SQL run
|
||||
store. HTTP admissions do not pass a `user_id`; the SQL store stamps the
|
||||
request user on the row, but the process-local run record kept `None`. A
|
||||
@ -2785,3 +2795,4 @@ with **180 merged pull requests** since the first 2.0 milestone tag.
|
||||
[#5357]: https://github.com/bytedance/deer-flow/pull/5357
|
||||
[#5393]: https://github.com/bytedance/deer-flow/pull/5393
|
||||
[#5401]: https://github.com/bytedance/deer-flow/pull/5401
|
||||
[#5403]: https://github.com/bytedance/deer-flow/pull/5403
|
||||
|
||||
@ -397,6 +397,13 @@
|
||||
|
||||
### 修复
|
||||
|
||||
- **模型:** 当 `supports_reasoning_effort: true` 的模型同时从 profile 获得
|
||||
`reasoning_effort`(顶层、`when_thinking_enabled` 或 `when_thinking_disabled`
|
||||
中,或由 `extra_body.thinking` 的关闭路径注入)时,lead agent 不再构建失败。lead
|
||||
agent 的常规构建总会转发请求的 effort(即使未设置),导致该参数两次传给 provider 构造函数,抛出
|
||||
`TypeError: got multiple values for keyword argument 'reasoning_effort'`。现在请求值
|
||||
按每个 agent 的 `model_settings` 方式叠加:替换 profile 顶层的值,未设置时保留该值,
|
||||
最终值仍由 thinking 模式相关设置决定。Codex 保留自己的级别校验。([#5403])
|
||||
- **运行时:** 带 `Idempotency-Key` 的 run 重试在 SQL run 存储上不再返回 500。HTTP
|
||||
准入不会传入 `user_id`,SQL 存储会把请求用户写入该行,但进程内的 run 记录仍为
|
||||
`None`。同一 key 的重试若落到另一个 Gateway worker,或在已完成的 run 被清理后回到
|
||||
@ -2135,3 +2142,4 @@ DeerFlow 2.0 是围绕"超级智能体"框架的彻底重写,核心包含子
|
||||
[#5357]: https://github.com/bytedance/deer-flow/pull/5357
|
||||
[#5393]: https://github.com/bytedance/deer-flow/pull/5393
|
||||
[#5401]: https://github.com/bytedance/deer-flow/pull/5401
|
||||
[#5403]: https://github.com/bytedance/deer-flow/pull/5403
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
- `create_chat_model(name, thinking_enabled)` instantiates LLM from config via reflection
|
||||
- Supports `thinking_enabled` flag with per-model `when_thinking_enabled` overrides
|
||||
- Supports vLLM-style thinking toggles via `when_thinking_enabled.extra_body.chat_template_kwargs.enable_thinking` for Qwen reasoning models, while normalizing legacy `thinking` configs for backward compatibility
|
||||
- A per-request `reasoning_effort` kwarg (the regular, non-bootstrap lead-agent build passes it even when `None`) is popped from `kwargs` and layered like `model_overrides`: a non-`None` value replaces the profile's, and the thinking transforms applied afterwards (`when_thinking_enabled`, `when_thinking_disabled`, the `extra_body.thinking` disable path) still decide the final value. Never let a key reach the constructor through both `kwargs` and the profile settings — Python raises `got multiple values for keyword argument` and the lead agent cannot be built for that model. Codex checks the requested level itself. Pinned by `tests/test_model_factory.py` and `tests/test_lead_agent_model_resolution.py`
|
||||
- Supports `supports_vision` flag for image understanding models
|
||||
- Config values starting with `$` resolved as environment variables
|
||||
- Missing provider modules surface actionable install hints from reflection resolvers (for example `uv add langchain-google-genai`)
|
||||
|
||||
@ -236,6 +236,17 @@ def create_chat_model(name: str | None = None, thinking_enabled: bool = False, *
|
||||
# value exactly as it would a profile-native one.
|
||||
if model_overrides:
|
||||
model_settings_from_config.update({key: value for key, value in model_overrides.items() if value is not None})
|
||||
# The per-request reasoning effort layers the same way. The regular lead-agent
|
||||
# build forwards the key even when None (neither the request nor the custom
|
||||
# agent chose one), so it must leave kwargs: a profile that also yields
|
||||
# reasoning_effort would otherwise hand the constructor the keyword twice.
|
||||
# Codex validates and maps the requested value itself below.
|
||||
from deerflow.models.openai_codex_provider import CodexChatModel
|
||||
|
||||
is_codex_model = issubclass(model_class, CodexChatModel)
|
||||
requested_reasoning_effort = kwargs.pop("reasoning_effort", None)
|
||||
if requested_reasoning_effort is not None and not is_codex_model:
|
||||
model_settings_from_config["reasoning_effort"] = requested_reasoning_effort
|
||||
# Compute effective when_thinking_enabled by merging in the `thinking` shortcut field.
|
||||
# The `thinking` shortcut is equivalent to setting when_thinking_enabled["thinking"].
|
||||
has_thinking_settings = (model_config.when_thinking_enabled is not None) or (model_config.thinking is not None)
|
||||
@ -269,7 +280,7 @@ def create_chat_model(name: str | None = None, thinking_enabled: bool = False, *
|
||||
# Native langchain_anthropic: thinking is a direct constructor parameter
|
||||
model_settings_from_config["thinking"] = {"type": "disabled"}
|
||||
if not model_config.supports_reasoning_effort:
|
||||
kwargs.pop("reasoning_effort", None)
|
||||
requested_reasoning_effort = None
|
||||
model_settings_from_config.pop("reasoning_effort", None)
|
||||
|
||||
# Normalize the api_base -> base_url alias FIRST, so the downstream OpenAI-compatible
|
||||
@ -278,18 +289,15 @@ def create_chat_model(name: str | None = None, thinking_enabled: bool = False, *
|
||||
_apply_stream_chunk_timeout_default(model_class, model_settings_from_config)
|
||||
|
||||
# For Codex Responses API models: map thinking mode to reasoning_effort
|
||||
from deerflow.models.openai_codex_provider import CodexChatModel
|
||||
|
||||
if issubclass(model_class, CodexChatModel):
|
||||
if is_codex_model:
|
||||
# The ChatGPT Codex endpoint currently rejects max_tokens/max_output_tokens.
|
||||
model_settings_from_config.pop("max_tokens", None)
|
||||
|
||||
# Use explicit reasoning_effort from frontend if provided (low/medium/high)
|
||||
explicit_effort = kwargs.pop("reasoning_effort", None)
|
||||
if not thinking_enabled:
|
||||
model_settings_from_config["reasoning_effort"] = "none"
|
||||
elif explicit_effort and explicit_effort in ("low", "medium", "high", "xhigh"):
|
||||
model_settings_from_config["reasoning_effort"] = explicit_effort
|
||||
elif requested_reasoning_effort in ("low", "medium", "high", "xhigh"):
|
||||
model_settings_from_config["reasoning_effort"] = requested_reasoning_effort
|
||||
elif "reasoning_effort" not in model_settings_from_config:
|
||||
model_settings_from_config["reasoning_effort"] = "medium"
|
||||
|
||||
|
||||
@ -287,6 +287,45 @@ def test_internal_make_lead_agent_uses_explicit_app_config(monkeypatch):
|
||||
assert result["model"] is not None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("runtime_options", "expected_effort"),
|
||||
[
|
||||
pytest.param({}, "high", id="request-leaves-effort-unset"),
|
||||
pytest.param({"reasoning_effort": "low"}, "low", id="request-chooses-effort"),
|
||||
],
|
||||
)
|
||||
def test_internal_make_lead_agent_builds_model_whose_profile_sets_reasoning_effort(monkeypatch, runtime_options, expected_effort):
|
||||
"""The regular lead-agent build forwards ``reasoning_effort`` even when it is
|
||||
None, so the real factory must merge it with a profile-level value instead of
|
||||
handing the constructor the keyword twice (which raised ``TypeError``)."""
|
||||
model = ModelConfig(
|
||||
name="effort-model",
|
||||
display_name="effort-model",
|
||||
description=None,
|
||||
use="langchain_openai:ChatOpenAI",
|
||||
model="effort-model",
|
||||
api_key="test-key",
|
||||
reasoning_effort="high",
|
||||
supports_thinking=False,
|
||||
supports_reasoning_effort=True,
|
||||
supports_vision=False,
|
||||
)
|
||||
app_config = _make_app_config([model])
|
||||
|
||||
import deerflow.tools as tools_module
|
||||
|
||||
monkeypatch.setattr(tools_module, "get_available_tools", lambda **kwargs: [])
|
||||
monkeypatch.setattr(lead_agent_module, "build_middlewares", lambda config, model_name, agent_name=None, **kwargs: [])
|
||||
monkeypatch.setattr(lead_agent_module, "create_agent", lambda **kwargs: kwargs)
|
||||
|
||||
result = lead_agent_module._make_lead_agent(
|
||||
{"configurable": {"model_name": "effort-model", **runtime_options}},
|
||||
app_config=app_config,
|
||||
)
|
||||
|
||||
assert result["model"].reasoning_effort == expected_effort
|
||||
|
||||
|
||||
@pytest.mark.parametrize("is_bootstrap", [False, True])
|
||||
def test_internal_make_lead_agent_selects_and_normalizes_delta_state(monkeypatch, is_bootstrap):
|
||||
app_config = _make_app_config([_make_model("delta-model", supports_thinking=False)])
|
||||
|
||||
@ -972,6 +972,35 @@ def test_codex_provider_defaults_reasoning_effort_to_medium(monkeypatch):
|
||||
assert FakeChatModel.captured_kwargs.get("reasoning_effort") == "medium"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("supports_reasoning_effort", "requested_effort"),
|
||||
[
|
||||
pytest.param(True, "minimal", id="value-outside-codex-levels"),
|
||||
pytest.param(False, "high", id="profile-without-effort-support"),
|
||||
],
|
||||
)
|
||||
def test_codex_provider_falls_back_to_medium_for_request_it_cannot_honor(monkeypatch, supports_reasoning_effort, requested_effort):
|
||||
"""Codex resolves the requested effort itself; the generic request layering
|
||||
must not smuggle a value past its level check or the capability guard."""
|
||||
cfg = _make_app_config(
|
||||
[
|
||||
_make_model(
|
||||
"codex",
|
||||
use="deerflow.models.openai_codex_provider:CodexChatModel",
|
||||
supports_thinking=True,
|
||||
supports_reasoning_effort=supports_reasoning_effort,
|
||||
)
|
||||
]
|
||||
)
|
||||
_patch_factory(monkeypatch, cfg, model_class=FakeCodexChatModel)
|
||||
monkeypatch.setattr(codex_provider_module, "CodexChatModel", FakeCodexChatModel)
|
||||
|
||||
FakeChatModel.captured_kwargs = {}
|
||||
factory_module.create_chat_model(name="codex", thinking_enabled=True, reasoning_effort=requested_effort)
|
||||
|
||||
assert FakeChatModel.captured_kwargs.get("reasoning_effort") == "medium"
|
||||
|
||||
|
||||
def test_codex_provider_strips_unsupported_max_tokens(monkeypatch):
|
||||
cfg = _make_app_config(
|
||||
[
|
||||
@ -1231,10 +1260,51 @@ def test_no_duplicate_kwarg_when_reasoning_effort_in_config_and_thinking_disable
|
||||
# Must not raise TypeError
|
||||
factory_module.create_chat_model(name="doubao-model", thinking_enabled=False)
|
||||
|
||||
# kwargs (runtime) takes precedence: thinking-disabled path sets reasoning_effort=minimal
|
||||
# The thinking-disabled path governs the profile value: it sets reasoning_effort=minimal
|
||||
assert captured.get("reasoning_effort") == "minimal"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("profile", "thinking_enabled", "requested_effort", "expected_effort"),
|
||||
[
|
||||
pytest.param({"reasoning_effort": "high"}, True, None, "high", id="unset-request-keeps-profile-value"),
|
||||
pytest.param({"reasoning_effort": "high"}, True, "low", "low", id="request-replaces-profile-value"),
|
||||
pytest.param({"when_thinking_enabled": {"reasoning_effort": "medium"}}, True, "high", "medium", id="thinking-enabled-settings-govern-request"),
|
||||
pytest.param({"when_thinking_enabled": {"extra_body": {"thinking": {"type": "enabled"}}}}, False, "high", "minimal", id="extra-body-disable-path-governs-request"),
|
||||
pytest.param({"when_thinking_disabled": {"reasoning_effort": "low"}}, False, "high", "low", id="thinking-disabled-settings-govern-request"),
|
||||
],
|
||||
)
|
||||
def test_requested_reasoning_effort_layers_over_profile_value(profile, thinking_enabled, requested_effort, expected_effort):
|
||||
"""The regular lead-agent build forwards ``reasoning_effort`` even when None
|
||||
(neither the request nor the custom agent chose one). When the profile also yields one,
|
||||
the real ChatOpenAI must still build instead of raising ``got multiple
|
||||
values for keyword argument 'reasoning_effort'``, and the request must layer
|
||||
like ``model_overrides``: it replaces a profile value, None never clobbers
|
||||
one, and the thinking settings still govern the result."""
|
||||
model = ModelConfig(
|
||||
name="effort-profile",
|
||||
display_name="Effort Profile",
|
||||
description=None,
|
||||
use="langchain_openai:ChatOpenAI",
|
||||
model="effort-profile",
|
||||
api_key="test-key",
|
||||
supports_thinking=True,
|
||||
supports_reasoning_effort=True,
|
||||
supports_vision=False,
|
||||
**profile,
|
||||
)
|
||||
|
||||
chat_model = factory_module.create_chat_model(
|
||||
name="effort-profile",
|
||||
thinking_enabled=thinking_enabled,
|
||||
reasoning_effort=requested_effort,
|
||||
app_config=_make_app_config([model]),
|
||||
attach_tracing=False,
|
||||
)
|
||||
|
||||
assert chat_model._get_request_payload([HumanMessage(content="ping")])["reasoning_effort"] == expected_effort
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# stream_chunk_timeout default injection (issue #3189)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user