deer-flow/backend/tests/test_agents_router_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

147 lines
5.2 KiB
Python

"""API tests for per-agent model settings (issue #4336).
Exercises the ``/api/agents`` create/update handlers directly: the new
``model_settings`` / ``thinking_enabled`` / ``reasoning_effort`` fields
persist and round-trip, an omitted field is preserved on update, and an
unknown ``model`` is rejected before it reaches the runtime.
"""
from __future__ import annotations
from pathlib import Path
import pytest
from fastapi import HTTPException
from app.gateway.routers.agents import (
AgentCreateRequest,
AgentUpdateRequest,
create_agent_endpoint,
get_agent,
update_agent,
)
from deerflow.config.agents_api_config import load_agents_api_config_from_dict
from deerflow.config.app_config import AppConfig, reset_app_config, set_app_config
from deerflow.config.model_config import ModelConfig
from deerflow.config.sandbox_config import SandboxConfig
pytestmark = pytest.mark.asyncio
@pytest.fixture
def _agent_env(tmp_path: Path, monkeypatch):
monkeypatch.setenv("DEER_FLOW_HOME", str(tmp_path))
monkeypatch.setattr("deerflow.config.paths._paths", None)
load_agents_api_config_from_dict({"enabled": True})
set_app_config(
AppConfig(
models=[ModelConfig(name="agent-model", display_name="Agent Model", description=None, use="langchain_openai:ChatOpenAI", model="agent-model")],
sandbox=SandboxConfig(use="deerflow.sandbox.local:LocalSandboxProvider"),
)
)
try:
yield
finally:
load_agents_api_config_from_dict({})
reset_app_config()
async def test_create_persists_model_settings(_agent_env) -> None:
resp = await create_agent_endpoint(
AgentCreateRequest(
name="researcher",
model="agent-model",
model_settings={"temperature": 0.2, "max_tokens": 12000},
thinking_enabled=True,
reasoning_effort="high",
soul="You are a researcher.",
)
)
assert resp.model == "agent-model"
assert resp.model_settings is not None
assert resp.model_settings.temperature == 0.2
assert resp.model_settings.max_tokens == 12000
assert resp.thinking_enabled is True
assert resp.reasoning_effort == "high"
# Reload through the read path to confirm it round-tripped to disk.
fetched = await get_agent("researcher")
assert fetched.model_settings is not None
assert fetched.model_settings.temperature == 0.2
assert fetched.reasoning_effort == "high"
async def test_create_rejects_unknown_model(_agent_env) -> None:
with pytest.raises(HTTPException) as excinfo:
await create_agent_endpoint(AgentCreateRequest(name="bad", model="ghost-model"))
assert excinfo.value.status_code == 422
async def test_update_preserves_unset_model_settings(_agent_env) -> None:
await create_agent_endpoint(
AgentCreateRequest(
name="researcher",
model="agent-model",
model_settings={"temperature": 0.2, "max_tokens": 12000},
thinking_enabled=True,
)
)
# Update only the description — model_settings / thinking must be preserved.
resp = await update_agent("researcher", AgentUpdateRequest(description="updated"))
assert resp.description == "updated"
assert resp.model_settings is not None
assert resp.model_settings.temperature == 0.2
assert resp.thinking_enabled is True
async def test_update_changes_model_settings(_agent_env) -> None:
await create_agent_endpoint(AgentCreateRequest(name="researcher", model="agent-model", model_settings={"temperature": 0.2}))
resp = await update_agent(
"researcher",
AgentUpdateRequest(model_settings={"temperature": 0.9, "max_tokens": 2048}),
)
assert resp.model_settings is not None
assert resp.model_settings.temperature == 0.9
assert resp.model_settings.max_tokens == 2048
async def test_update_model_settings_merges_omitted_subfields(_agent_env) -> None:
await create_agent_endpoint(
AgentCreateRequest(
name="researcher",
model="agent-model",
model_settings={"temperature": 0.2, "max_tokens": 12000},
)
)
resp = await update_agent("researcher", AgentUpdateRequest(model_settings={"temperature": 0.9}))
assert resp.model_settings is not None
assert resp.model_settings.temperature == 0.9
assert resp.model_settings.max_tokens == 12000
async def test_update_model_settings_null_subfield_clears_only_that_subfield(_agent_env) -> None:
await create_agent_endpoint(
AgentCreateRequest(
name="researcher",
model="agent-model",
model_settings={"temperature": 0.2, "max_tokens": 12000},
)
)
resp = await update_agent("researcher", AgentUpdateRequest(model_settings={"max_tokens": None}))
assert resp.model_settings is not None
assert resp.model_settings.temperature == 0.2
assert resp.model_settings.max_tokens is None
async def test_update_rejects_unknown_model(_agent_env) -> None:
await create_agent_endpoint(AgentCreateRequest(name="researcher", model="agent-model"))
with pytest.raises(HTTPException) as excinfo:
await update_agent("researcher", AgentUpdateRequest(model="ghost-model"))
assert excinfo.value.status_code == 422