mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-07-27 16:37:55 +00:00
* 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.
147 lines
5.2 KiB
Python
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
|