fix(runtime): persist the effective model name for every run (#6980)

run_agent() only refreshed runs.model_name when the request named a model,
and it read the effective model from agent.metadata, which compiled
LangGraph graphs do not have. The default model (no model_name requested)
and fallback models were therefore never persisted. Use the effective
model that lead-agent assembly already resolves, and write only when it
differs from the stored value.

Supersedes #3110. Fixes #3109.

Co-authored-by: Yi Tang <6054101+yitang@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
TAI-YUAN GUO 2026-10-18 11:10:38 +08:00 committed by GitHub
parent 8d5f5fd0a8
commit a1349a1fe1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 65 additions and 10 deletions

View File

@ -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:

View File

@ -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.