Repository navigation
OpenAI-compat engine drops assistant tool_calls from replayed history, producing protocol-invalid message sequences #2198
Description
Summary
OpenAiMessage serializes only role/content/tool_call_id (openai_compat_inference.rs:144-150); the ReAct loop persists the assistant turn that issued tool calls as content: "" with structured tool_calls (agent_loop.rs:1686-1691), so on the OpenAI-compat path every replayed assistant tool-call turn becomes {"role":"assistant","content":""} followed by {"role":"tool","tool_call_id":...} with no preceding tool_calls, and the tool message's name field is dropped too.
Failure scenario
On strict servers (the real OpenAI API — an explicitly supported configuration, see the gpt-4o test at line 567 — or recent vLLM): the model makes one tool call in ReAct round 1; round 2's request replays the malformed pair and the endpoint returns HTTP 400 (messages with role "tool" must be a response to a preceding message with "tool_calls"). Every tool-using conversation hard-fails immediately after the first tool execution, surfacing only an opaque engine error.
On lax servers (Ollama /v1, LM Studio): the request is accepted, but the model can never see which tool it called with which arguments — degrading multi-step tool use and defeating the duplicate-write guard's informative-result design, which assumes the model can read its own prior calls.
Location
packages/agent/src/local_agent/openai_compat_inference.rs:144-150 (the struct) and its use at :226.
This fix needs no API key — verify against local Ollama
The defect is entirely in what NodeSpace serializes, so it is verifiable without any hosted provider:
-
Unit test on the outgoing shape (primary evidence, no network). Build a history containing an assistant tool-call turn plus its tool result, serialize, and assert:
tool_callsis present on the assistant message, withid,type: "function", andfunction.{name,arguments}nameis present on thetoolmessagetool_call_idstill pairs the result to its call
-
Local Ollama for the round-trip.
packages/agent/tests/live_openai_compat_smoke.rsalready targetshttp://127.0.0.1:11434/v1:cargo test -p nodespace-agent --test live_openai_compat_smoke -- --ignored --nocaptureOllama is enough to prove a multi-turn tool conversation still works after the change, and that nothing regressed on the path that ships today.
⚠️ But a green Ollama run is not evidence the bug is fixed. Ollama is one of the lax servers — it accepts the malformed shape today, which is precisely why this went unnoticed while every prior measurement ran against local Ollama. Ollama proves no regression; the unit test proves the fix. -
NODESPACE_PROMPT_DUMP=<path>captures the exact outgoing request JSON on this path (openai_compat_prompt_dump.rs, records tagged"engine": "openai_compat"). Reading the dumped payload is the direct way to confirm the wire shape rather than inferring it.
Strict-server confirmation is out of scope here. That belongs to #2189, which provisions a real key from a hosted OpenAI-compatible provider (AI Studio) and exercises the path end-to-end through the GUI. This issue lands first so #2189 is not blocked by a guaranteed HTTP 400 after its first tool call.
Acceptance criteria
-
OpenAiMessagecarriestool_callson assistant messages, matching the OpenAI wire contract - The
toolmessage'snamefield is preserved - Unit test asserting the serialized shape for an assistant-tool-call + tool-result pair
-
live_openai_compat_smoke.rsstill passes against local Ollama (no regression on the shipping path) - No API key or hosted provider required to verify any of the above
Severity
important — blocks #2189 and breaks every tool-using conversation against strict providers.
Found via a full-repo adversarial bug sweep, independently verified by two separate skeptical reviewers reading the actual current code before filing.
Activity
Serialize assistant tool_calls and tool-result name on the OpenAI-com…
mstomar125 commentedon Sep 3, 2026
Independently re-verified before starting today's drain: PR#2236 ("Serialize assistant tool_calls and tool-result name on the OpenAI-compat path") is merged, reviewed, and additionally verified against a real Ollama tool-calling conversation. Confirmed live in
openai_compat_inference.rs—OpenAiMessagenow carriestool_calls. Board status was stuck on Todo despite the real GitHub closure — fixing that now.