diff --git a/backend/packages/harness/deerflow/runtime/runs/worker.py b/backend/packages/harness/deerflow/runtime/runs/worker.py index e3543c15ae..87f2ead9bf 100644 --- a/backend/packages/harness/deerflow/runtime/runs/worker.py +++ b/backend/packages/harness/deerflow/runtime/runs/worker.py @@ -1395,16 +1395,11 @@ async def run_agent( graph_input["goal"] = Overwrite(copy.deepcopy(current_values.get("goal"))) graph_input["goal_outcome"] = None - # Capture the effective (resolved) model name from the agent's metadata. - # _resolve_model_name in agent.py may return the default model if the - # requested name is not in the allowlist — this update ensures the - # persisted model_name reflects the actual model used. - if record.model_name is not None: - resolved = getattr(agent, "metadata", {}) or {} - if isinstance(resolved, dict): - effective = resolved.get("model_name") - if effective and effective != record.model_name: - await run_manager.update_model_name(record.run_id, effective) + # Persist the model assembly resolved — the default when the request + # named none, or a fallback when the requested model is unavailable — + # so the stored model_name reflects the actual model used (#3109). + if effective_model and effective_model != record.model_name: + await run_manager.update_model_name(record.run_id, effective_model) # 4. Attach checkpointer and store if checkpointer is not None: diff --git a/backend/tests/test_worker_langfuse_metadata.py b/backend/tests/test_worker_langfuse_metadata.py index cda608efef..92e04b22eb 100644 --- a/backend/tests/test_worker_langfuse_metadata.py +++ b/backend/tests/test_worker_langfuse_metadata.py @@ -210,6 +210,66 @@ async def test_run_agent_tags_the_effective_model(monkeypatch, requested_model): assert "model:missing-model" not in tags +class _ModelNameRecordingRunManager(_FakeRunManager): + def __init__(self) -> None: + self.model_name_updates: list[tuple[str, str | None]] = [] + + async def update_model_name(self, run_id: str, model_name: str | None) -> None: + self.model_name_updates.append((run_id, model_name)) + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + ("requested_model", "expected_updates"), + [ + (None, [("run-persist-model", "actual-model")]), + ("missing-model", [("run-persist-model", "actual-model")]), + ("actual-model", []), + ], +) +async def test_run_agent_persists_the_effective_model(monkeypatch, requested_model, expected_updates): + """The run record stores the model that ran, including when none was requested (#3109).""" + from deerflow.agents.lead_agent import agent as lead_agent_module + from deerflow.config.app_config import AppConfig + from deerflow.config.model_config import ModelConfig + from deerflow.config.sandbox_config import SandboxConfig + + fake_agent = _FakeAgent() + app_config = AppConfig( + models=[ModelConfig(name="actual-model", model="actual-model", use="langchain_openai:ChatOpenAI")], + sandbox=SandboxConfig(use="deerflow.sandbox.local:LocalSandboxProvider"), + ) + monkeypatch.setattr(lead_agent_module, "create_chat_model", lambda **kwargs: object()) + monkeypatch.setattr(lead_agent_module, "create_agent", lambda **kwargs: fake_agent) + + def agent_factory(config): + return lead_agent_module.assemble_lead_agent(config, app_config=app_config) + + record = RunRecord( + run_id="run-persist-model", + thread_id="thread-persist-model", + assistant_id="lead-agent", + status=RunStatus.pending, + on_disconnect=DisconnectMode.cancel, + model_name=requested_model, + ) + record.abort_event = asyncio.Event() + run_manager = _ModelNameRecordingRunManager() + + await run_agent( + _FakeBridge(), + run_manager, + record, + ctx=RunContext(checkpointer=None, app_config=app_config), + agent_factory=agent_factory, + graph_input={"messages": []}, + config={"configurable": {"thread_id": record.thread_id, "model_name": requested_model}}, + ) + + assert fake_agent.captured_config is not None, "astream was not invoked" + assert run_manager.model_name_updates == expected_updates + + @pytest.mark.asyncio async def test_run_agent_uses_context_user_id_over_contextvar(monkeypatch): """A run carrying ``context.user_id`` traces to that user, not the contextvar.