mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-04-25 11:18:22 +00:00
fix(gateway): merge context field into configurable for langgraph-compat runs (#1699)
The langgraph-compat layer dropped the DeerFlow-specific `context` field from run requests, causing agent config (subagent_enabled, is_plan_mode, thinking_enabled, etc.) to fall back to defaults. Add `context` to RunCreateRequest and merge allowlisted keys into config.configurable in start_run, with existing configurable values taking precedence. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3e461d9d08
commit
4383d96583
@ -38,6 +38,7 @@ class RunCreateRequest(BaseModel):
|
|||||||
command: dict[str, Any] | None = Field(default=None, description="LangGraph Command")
|
command: dict[str, Any] | None = Field(default=None, description="LangGraph Command")
|
||||||
metadata: dict[str, Any] | None = Field(default=None, description="Run metadata")
|
metadata: dict[str, Any] | None = Field(default=None, description="Run metadata")
|
||||||
config: dict[str, Any] | None = Field(default=None, description="RunnableConfig overrides")
|
config: dict[str, Any] | None = Field(default=None, description="RunnableConfig overrides")
|
||||||
|
context: dict[str, Any] | None = Field(default=None, description="DeerFlow context overrides (model_name, thinking_enabled, etc.)")
|
||||||
webhook: str | None = Field(default=None, description="Completion callback URL")
|
webhook: str | None = Field(default=None, description="Completion callback URL")
|
||||||
checkpoint_id: str | None = Field(default=None, description="Resume from checkpoint")
|
checkpoint_id: str | None = Field(default=None, description="Resume from checkpoint")
|
||||||
checkpoint: dict[str, Any] | None = Field(default=None, description="Full checkpoint object")
|
checkpoint: dict[str, Any] | None = Field(default=None, description="Full checkpoint object")
|
||||||
|
|||||||
@ -271,6 +271,27 @@ async def start_run(
|
|||||||
agent_factory = resolve_agent_factory(body.assistant_id)
|
agent_factory = resolve_agent_factory(body.assistant_id)
|
||||||
graph_input = normalize_input(body.input)
|
graph_input = normalize_input(body.input)
|
||||||
config = build_run_config(thread_id, body.config, body.metadata, assistant_id=body.assistant_id)
|
config = build_run_config(thread_id, body.config, body.metadata, assistant_id=body.assistant_id)
|
||||||
|
|
||||||
|
# Merge DeerFlow-specific context overrides into configurable.
|
||||||
|
# The ``context`` field is a custom extension for the langgraph-compat layer
|
||||||
|
# that carries agent configuration (model_name, thinking_enabled, etc.).
|
||||||
|
# Only agent-relevant keys are forwarded; unknown keys (e.g. thread_id) are ignored.
|
||||||
|
context = getattr(body, "context", None)
|
||||||
|
if context:
|
||||||
|
_CONTEXT_CONFIGURABLE_KEYS = {
|
||||||
|
"model_name",
|
||||||
|
"mode",
|
||||||
|
"thinking_enabled",
|
||||||
|
"reasoning_effort",
|
||||||
|
"is_plan_mode",
|
||||||
|
"subagent_enabled",
|
||||||
|
"max_concurrent_subagents",
|
||||||
|
}
|
||||||
|
configurable = config.setdefault("configurable", {})
|
||||||
|
for key in _CONTEXT_CONFIGURABLE_KEYS:
|
||||||
|
if key in context:
|
||||||
|
configurable.setdefault(key, context[key])
|
||||||
|
|
||||||
stream_modes = normalize_stream_modes(body.stream_mode)
|
stream_modes = normalize_stream_modes(body.stream_mode)
|
||||||
|
|
||||||
task = asyncio.create_task(
|
task = asyncio.create_task(
|
||||||
|
|||||||
@ -160,3 +160,127 @@ def test_resolve_agent_factory_returns_make_lead_agent():
|
|||||||
assert resolve_agent_factory("lead_agent") is make_lead_agent
|
assert resolve_agent_factory("lead_agent") is make_lead_agent
|
||||||
assert resolve_agent_factory("finalis") is make_lead_agent
|
assert resolve_agent_factory("finalis") is make_lead_agent
|
||||||
assert resolve_agent_factory("custom-agent-123") is make_lead_agent
|
assert resolve_agent_factory("custom-agent-123") is make_lead_agent
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Regression tests for issue #1699:
|
||||||
|
# context field in langgraph-compat requests not merged into configurable
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_run_create_request_accepts_context():
|
||||||
|
"""RunCreateRequest must accept the ``context`` field without dropping it."""
|
||||||
|
from app.gateway.routers.thread_runs import RunCreateRequest
|
||||||
|
|
||||||
|
body = RunCreateRequest(
|
||||||
|
input={"messages": [{"role": "user", "content": "hi"}]},
|
||||||
|
context={
|
||||||
|
"model_name": "deepseek-v3",
|
||||||
|
"thinking_enabled": True,
|
||||||
|
"is_plan_mode": True,
|
||||||
|
"subagent_enabled": True,
|
||||||
|
"thread_id": "some-thread-id",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert body.context is not None
|
||||||
|
assert body.context["model_name"] == "deepseek-v3"
|
||||||
|
assert body.context["is_plan_mode"] is True
|
||||||
|
assert body.context["subagent_enabled"] is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_run_create_request_context_defaults_to_none():
|
||||||
|
"""RunCreateRequest without context should default to None (backward compat)."""
|
||||||
|
from app.gateway.routers.thread_runs import RunCreateRequest
|
||||||
|
|
||||||
|
body = RunCreateRequest(input=None)
|
||||||
|
assert body.context is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_context_merges_into_configurable():
|
||||||
|
"""Context values must be merged into config['configurable'] by start_run.
|
||||||
|
|
||||||
|
Since start_run is async and requires many dependencies, we test the
|
||||||
|
merging logic directly by simulating what start_run does.
|
||||||
|
"""
|
||||||
|
from app.gateway.services import build_run_config
|
||||||
|
|
||||||
|
# Simulate the context merging logic from start_run
|
||||||
|
config = build_run_config("thread-1", None, None)
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"model_name": "deepseek-v3",
|
||||||
|
"mode": "ultra",
|
||||||
|
"reasoning_effort": "high",
|
||||||
|
"thinking_enabled": True,
|
||||||
|
"is_plan_mode": True,
|
||||||
|
"subagent_enabled": True,
|
||||||
|
"max_concurrent_subagents": 5,
|
||||||
|
"thread_id": "should-be-ignored",
|
||||||
|
}
|
||||||
|
|
||||||
|
_CONTEXT_CONFIGURABLE_KEYS = {
|
||||||
|
"model_name",
|
||||||
|
"mode",
|
||||||
|
"thinking_enabled",
|
||||||
|
"reasoning_effort",
|
||||||
|
"is_plan_mode",
|
||||||
|
"subagent_enabled",
|
||||||
|
"max_concurrent_subagents",
|
||||||
|
}
|
||||||
|
configurable = config.setdefault("configurable", {})
|
||||||
|
for key in _CONTEXT_CONFIGURABLE_KEYS:
|
||||||
|
if key in context:
|
||||||
|
configurable.setdefault(key, context[key])
|
||||||
|
|
||||||
|
assert config["configurable"]["model_name"] == "deepseek-v3"
|
||||||
|
assert config["configurable"]["thinking_enabled"] is True
|
||||||
|
assert config["configurable"]["is_plan_mode"] is True
|
||||||
|
assert config["configurable"]["subagent_enabled"] is True
|
||||||
|
assert config["configurable"]["max_concurrent_subagents"] == 5
|
||||||
|
assert config["configurable"]["reasoning_effort"] == "high"
|
||||||
|
assert config["configurable"]["mode"] == "ultra"
|
||||||
|
# thread_id from context should NOT override the one from build_run_config
|
||||||
|
assert config["configurable"]["thread_id"] == "thread-1"
|
||||||
|
# Non-allowlisted keys should not appear
|
||||||
|
assert "thread_id" not in {k for k in context if k in _CONTEXT_CONFIGURABLE_KEYS}
|
||||||
|
|
||||||
|
|
||||||
|
def test_context_does_not_override_existing_configurable():
|
||||||
|
"""Values already in config.configurable must NOT be overridden by context.
|
||||||
|
|
||||||
|
This ensures that explicit configurable values from the ``config`` field
|
||||||
|
take precedence over the ``context`` field.
|
||||||
|
"""
|
||||||
|
from app.gateway.services import build_run_config
|
||||||
|
|
||||||
|
config = build_run_config(
|
||||||
|
"thread-1",
|
||||||
|
{"configurable": {"model_name": "gpt-4", "is_plan_mode": False}},
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"model_name": "deepseek-v3",
|
||||||
|
"is_plan_mode": True,
|
||||||
|
"subagent_enabled": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
_CONTEXT_CONFIGURABLE_KEYS = {
|
||||||
|
"model_name",
|
||||||
|
"mode",
|
||||||
|
"thinking_enabled",
|
||||||
|
"reasoning_effort",
|
||||||
|
"is_plan_mode",
|
||||||
|
"subagent_enabled",
|
||||||
|
"max_concurrent_subagents",
|
||||||
|
}
|
||||||
|
configurable = config.setdefault("configurable", {})
|
||||||
|
for key in _CONTEXT_CONFIGURABLE_KEYS:
|
||||||
|
if key in context:
|
||||||
|
configurable.setdefault(key, context[key])
|
||||||
|
|
||||||
|
# Existing values must NOT be overridden
|
||||||
|
assert config["configurable"]["model_name"] == "gpt-4"
|
||||||
|
assert config["configurable"]["is_plan_mode"] is False
|
||||||
|
# New values should be added
|
||||||
|
assert config["configurable"]["subagent_enabled"] is True
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user