deer-flow/backend/tests/test_agent_model_settings.py
Ryker_Feng 20debf9cc7
feat(agents): per-agent model and generation settings (#4347)
* feat(agents): per-agent model and generation settings

Let each custom agent choose its own model and sampling settings
(temperature, max_tokens) plus thinking / reasoning_effort defaults,
so agents sharing a model profile are no longer stuck with one shared
temperature and output length (#4336).

AgentConfig gains optional model_settings / thinking_enabled /
reasoning_effort (None = inherit). create_chat_model applies per-caller
model_overrides on top of the profile before the thinking/Codex
transforms; the lead agent resolves each knob with precedence
request > agent config > profile/default. The /api/agents create/update
routes persist the fields and reject an unknown model. The default lead
agent path is unchanged (no agent config -> overrides None). The agent
chat composer also stops force-overriding an agent's configured default
model with models[0].

* fix(agents): tri-state thinking control and default-model capability gating

The model-settings dialog seeded the thinking switch to false, so opening
it to tweak temperature and saving silently disabled thinking (the runtime
default is on) with no way back to inherit. It also hid the thinking /
reasoning controls whenever the agent inherited the global default model,
since `__default__` never resolved through `models.find`.

Give thinking an explicit Inherit / On / Off tri-state so an untouched save
is a no-op, and resolve `__default__` to the effective default (models[0])
for the capability check. Logic lives in the tested helpers module.
2026-07-22 13:44:55 +08:00

145 lines
5.0 KiB
Python

"""Tests for the per-agent model-settings block on :class:`AgentConfig` (#4336).
Covers the new ``model_settings`` / ``thinking_enabled`` / ``reasoning_effort``
fields: validation bounds, YAML round-trip via ``load_agent_config``, backward
compatibility (absent = None), and that they are treated as managed fields by
``preserve_non_managed_fields``.
"""
from __future__ import annotations
from pathlib import Path
import pytest
import yaml
from pydantic import ValidationError
from deerflow.config.agents_config import (
MANAGED_AGENT_CONFIG_FIELDS,
MAX_AGENT_OUTPUT_TOKENS,
AgentConfig,
AgentModelSettings,
load_agent_config,
preserve_non_managed_fields,
)
def test_model_settings_default_to_none() -> None:
cfg = AgentConfig(name="solo")
assert cfg.model_settings is None
assert cfg.thinking_enabled is None
assert cfg.reasoning_effort is None
def test_model_settings_parse_full_shape() -> None:
cfg = AgentConfig(
name="researcher",
model_settings={"temperature": 0.2, "max_tokens": 12000},
thinking_enabled=True,
reasoning_effort="high",
)
assert isinstance(cfg.model_settings, AgentModelSettings)
assert cfg.model_settings.temperature == 0.2
assert cfg.model_settings.max_tokens == 12000
assert cfg.thinking_enabled is True
assert cfg.reasoning_effort == "high"
@pytest.mark.parametrize("temperature", [-0.1, 2.1])
def test_temperature_out_of_range_rejected(temperature: float) -> None:
with pytest.raises(ValidationError):
AgentModelSettings(temperature=temperature)
def test_max_tokens_must_be_positive() -> None:
with pytest.raises(ValidationError):
AgentModelSettings(max_tokens=0)
def test_max_tokens_has_sane_upper_bound() -> None:
with pytest.raises(ValidationError):
AgentModelSettings(max_tokens=MAX_AGENT_OUTPUT_TOKENS + 1)
def test_model_settings_forbids_unknown_keys() -> None:
with pytest.raises(ValidationError):
AgentModelSettings(top_p=0.9) # type: ignore[call-arg]
def test_reasoning_effort_rejects_unknown_value() -> None:
with pytest.raises(ValidationError):
AgentConfig(name="x", reasoning_effort="turbo") # type: ignore[arg-type]
def test_reasoning_effort_rejects_codex_unsupported_minimal() -> None:
with pytest.raises(ValidationError):
AgentConfig(name="x", reasoning_effort="minimal") # type: ignore[arg-type]
def test_model_settings_are_managed_fields() -> None:
# These are managed by the HTTP agent settings API, so the generic
# preserve_non_managed_fields helper must not also carry them. Surfaces that
# do not expose them directly, such as the harness update_agent tool, need a
# dedicated carry-forward path instead.
for field in ("model_settings", "thinking_enabled", "reasoning_effort"):
assert field in MANAGED_AGENT_CONFIG_FIELDS
def test_preserve_non_managed_excludes_model_settings() -> None:
cfg = AgentConfig(
name="a",
model_settings={"temperature": 0.5},
thinking_enabled=True,
github={"installation_id": 7},
)
preserved = preserve_non_managed_fields(cfg)
assert "model_settings" not in preserved
assert "thinking_enabled" not in preserved
# github stays preserved (hand-authored, not managed by the update surfaces).
assert "github" in preserved
def _write_agent(base: Path, user_id: str, name: str, body: dict) -> None:
agent_dir = base / "users" / user_id / "agents" / name
agent_dir.mkdir(parents=True, exist_ok=True)
(agent_dir / "config.yaml").write_text(yaml.safe_dump(body), encoding="utf-8")
def test_load_agent_config_round_trips_model_settings(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("DEER_FLOW_HOME", str(tmp_path))
from deerflow.config import paths as paths_module
monkeypatch.setattr(paths_module, "_paths", None)
body = {
"name": "researcher",
"model": "claude-sonnet-4-6",
"model_settings": {"temperature": 0.2, "max_tokens": 12000},
"thinking_enabled": True,
"reasoning_effort": "high",
}
_write_agent(tmp_path, "default", "researcher", body)
cfg = load_agent_config("researcher", user_id="default")
assert cfg is not None
assert cfg.model_settings is not None
assert cfg.model_settings.temperature == 0.2
assert cfg.model_settings.max_tokens == 12000
assert cfg.thinking_enabled is True
assert cfg.reasoning_effort == "high"
def test_load_agent_config_without_model_settings_is_none(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("DEER_FLOW_HOME", str(tmp_path))
from deerflow.config import paths as paths_module
monkeypatch.setattr(paths_module, "_paths", None)
_write_agent(tmp_path, "default", "plain", {"name": "plain", "model": "gpt-4o"})
cfg = load_agent_config("plain", user_id="default")
assert cfg is not None
assert cfg.model_settings is None
assert cfg.thinking_enabled is None
assert cfg.reasoning_effort is None