fix(code): offload create_model in server graph factory to unblock Codex (#4324)

Fix `Blocking call to os.mkdir` server startup failure when using
`openai_codex` (ChatGPT) models.

---

The async langgraph dev server graph factory
(`server_graph._make_graph`) called `create_model` synchronously on the
event loop. For the `openai_codex` provider, `create_model` →
`build_chat_model` → `provider.get_token()` acquires a cross-process
file lock that *unconditionally* calls `os.mkdir` (on every call,
regardless of token freshness). The dev server runs `blockbuster`, which
rejects blocking IO on the loop, so graph readiness fails with a 500:
`Blocking call to os.mkdir`. This made the server unstartable for
ChatGPT/Codex users, and re-running `/auth` did not help because the
`mkdir` is unconditional. Fix: offload `create_model` via
`asyncio.to_thread`, matching the existing pattern in
`configurable_model._apply_overrides_async`.

Made by [Open
SWE](https://openswe.vercel.app/agents/2b7c33d3-d7a8-d82b-c254-affd26a14334)

## References
- Plan:
https://openswe.vercel.app/agents/2b7c33d3-d7a8-d82b-c254-affd26a14334/plan

---------

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
This commit is contained in:
Mason Daugherty
2026-06-26 14:20:43 -04:00
committed by GitHub
parent b669f37fb0
commit 064ea0c685
2 changed files with 18 additions and 2 deletions
+7 -1
View File
@@ -163,7 +163,13 @@ async def _make_graph() -> Any: # noqa: ANN401
if project_context is not None:
settings.reload_from_environment(start_path=project_context.user_cwd)
result = create_model(config.model, extra_kwargs=config.model_params)
# Offload to a worker thread: `create_model` does blocking disk IO for some
# providers (e.g. the `openai_codex` token store currently acquires a file
# lock via `langchain-openai` that calls `os.mkdir`), which `blockbuster`
# rejects on the server event loop.
result = await asyncio.to_thread(
create_model, config.model, extra_kwargs=config.model_params
)
result.apply_to_settings()
tools, mcp_server_info = await _build_tools(config, project_context)
@@ -77,12 +77,17 @@ class TestServerGraph:
mcp_server_info = [SimpleNamespace(name="docs")]
loop_thread_id = threading.get_ident()
create_cli_agent_thread_ids: list[int] = []
create_model_thread_ids: list[int] = []
warm_import_thread_ids: list[int] = []
def create_cli_agent_side_effect(**_: object) -> tuple[object, object]:
create_cli_agent_thread_ids.append(threading.get_ident())
return graph_obj, object()
def create_model_side_effect(*_: object, **__: object) -> object:
create_model_thread_ids.append(threading.get_ident())
return model_result
def warm_import_side_effect() -> None:
warm_import_thread_ids.append(threading.get_ident())
@@ -100,7 +105,7 @@ class TestServerGraph:
)
config_module = _module_with_attrs(
"deepagents_code.config",
create_model=MagicMock(return_value=model_result),
create_model=MagicMock(side_effect=create_model_side_effect),
settings=SimpleNamespace(
has_tavily=False,
reload_from_environment=MagicMock(),
@@ -169,6 +174,11 @@ class TestServerGraph:
assert warm_import_thread_ids[0] != loop_thread_id
assert create_cli_agent_thread_ids
assert create_cli_agent_thread_ids[0] != loop_thread_id
# `create_model` must run off the loop thread: it does blocking disk IO
# for some providers (e.g. the `openai_codex` token store calls
# `os.mkdir`), which `blockbuster` rejects on the server event loop.
assert create_model_thread_ids
assert create_model_thread_ids[0] != loop_thread_id
kwargs = resolve_mcp_tools.await_args_list[0].kwargs
assert kwargs["explicit_config_path"] is None
assert kwargs["no_mcp"] is False