Summary
microsoft-foundry/deepseek-v4-pro now works for plain chat in OpenClaw, but tool-using/operator turns remain unreliable because DeepSeek/Foundry sometimes emits DSML tool markup as plain assistant text instead of native tool_calls, and the current openai-completions stream path does not recover that text into executable tool calls.
What changed
A prior fix already addressed the earlier request-shaping failure where Foundry rejected DeepSeek-style thinking payloads (400 Unrecognized request argument supplied: thinking). Plain chat/reasoning calls now succeed.
The remaining failure is later in the pipeline: tool intent is sometimes surfaced as text instead of becoming toolCall blocks.
Live evidence
Installed runtime checked locally:
openclaw --version -> OpenClaw 2026.5.20 (e510042)
Plain response smoke tests succeeded:
openclaw agent --local --agent main --model microsoft-foundry/deepseek-v4-pro --thinking minimal --message 'Reply with exactly DEEPSEEK_SMOKE_OK' --json
openclaw agent --local --agent main --model microsoft-foundry/deepseek-v4-pro --thinking medium --message 'Reply with exactly DEEPSEEK_MEDIUM_OK' --json
Tool-use repro against the real CLI/runtime:
openclaw agent --local --agent main --model microsoft-foundry/deepseek-v4-pro --thinking minimal --message 'Call the session_status tool exactly once with sessionKey current and then stop.' --json
Observed assistant payload returned as visible text instead of an executed tool call:
<|DSML|tool_calls>
<|DSML|invoke name="session_status">
<|DSML|parameter name="sessionKey" string="true">current</|DSML|parameter>
</|DSML|invoke>
</|DSML|tool_calls>
A second live variant was malformed/incomplete and omitted the parameter body entirely:
<|DSML|tool_calls>
<|DSML|invoke name="session_status">
</|DSML|invoke>
</|DSML|tool_calls>
Subagent/history verification also showed that the tool was not actually executed; the markup was emitted as assistant text.
Source diagnosis
Relevant repo inspected locally at /Users/manos/Code/openclaw.
What the current code does
src/agents/deepseek-text-filter.ts contains createDeepSeekTextFilter() which strips DSML wrapper text from visible output.
src/agents/openai-transport-stream.ts uses that filter in processOpenAICompletionsStream(...), but real tool calls are still created only from native choiceDelta.tool_calls.
So today the behavior is:
- if DeepSeek/Foundry emits native
tool_calls, OpenClaw can handle them
- if DeepSeek/Foundry emits DSML tool markup as plain text, OpenClaw currently strips/streams text but does not promote it to
toolCall
Why this is likely not just expected model behavior
DeepSeek's own tool-calls docs describe OpenAI-compatible native message.tool_calls as the intended path:
There is also upstream evidence that DeepSeek V4 Pro can intermittently emit tool calls as plain text with tool_calls: null:
So the remaining problem looks like a combination of:
- upstream/provider instability in DeepSeek tool emission
- missing defensive recovery in OpenClaw for DSML/textual tool intent on the
openai-completions stream path
Tested patch candidate
I added a conservative local patch in src/agents/openai-transport-stream.ts plus regression tests in src/agents/openai-transport-stream.test.ts that recover the observed DSML form into synthetic toolCall blocks when:
- the DSML block is complete
- the invoke name exists
- parameters or JSON body parse into valid object arguments
Added test coverage for:
- one-chunk observed DSML tool text
- split-chunk observed DSML tool text
- coexistence with existing DeepSeek DSML filtering/native-tool-call tests
Targeted validation passed:
npx vitest run src/agents/openai-transport-stream.test.ts -t "recovers observed DeepSeek DSML parameter tool call|filters DeepSeek DSML content without disturbing native tool calls|keeps DeepSeek DSML state across native tool-call chunks"
Result:
- 2 test files passed
- 8 tests passed
- 0 failures in that targeted set
Important caveat
This is currently a source-tree patch candidate, not a fully validated installed-runtime fix yet. The local openclaw CLI I reproed with is still the installed build and did not automatically pick up the edited source tree, so I have not yet re-validated the runtime end-to-end on a build containing the patch.
Suggested next step
Take this as a bug + patch candidate:
- add a conservative DSML-text-to-toolCall recovery layer in
processOpenAICompletionsStream(...) (or a provider-specific wrapper)
- only synthesize a tool call when the DSML block is complete and arguments parse cleanly
- leave malformed/incomplete DSML as plain text
- then validate from an actual build path using the patched source rather than the currently installed binary
Labels / framing suggestion
- bug
- providers
- microsoft-foundry
- deepseek
- streaming
- tool-calls
Summary
microsoft-foundry/deepseek-v4-pronow works for plain chat in OpenClaw, but tool-using/operator turns remain unreliable because DeepSeek/Foundry sometimes emits DSML tool markup as plain assistant text instead of nativetool_calls, and the currentopenai-completionsstream path does not recover that text into executable tool calls.What changed
A prior fix already addressed the earlier request-shaping failure where Foundry rejected DeepSeek-style
thinkingpayloads (400 Unrecognized request argument supplied: thinking). Plain chat/reasoning calls now succeed.The remaining failure is later in the pipeline: tool intent is sometimes surfaced as text instead of becoming
toolCallblocks.Live evidence
Installed runtime checked locally:
openclaw --version->OpenClaw 2026.5.20 (e510042)Plain response smoke tests succeeded:
openclaw agent --local --agent main --model microsoft-foundry/deepseek-v4-pro --thinking minimal --message 'Reply with exactly DEEPSEEK_SMOKE_OK' --jsonopenclaw agent --local --agent main --model microsoft-foundry/deepseek-v4-pro --thinking medium --message 'Reply with exactly DEEPSEEK_MEDIUM_OK' --jsonTool-use repro against the real CLI/runtime:
openclaw agent --local --agent main --model microsoft-foundry/deepseek-v4-pro --thinking minimal --message 'Call the session_status tool exactly once with sessionKey current and then stop.' --jsonObserved assistant payload returned as visible text instead of an executed tool call:
A second live variant was malformed/incomplete and omitted the parameter body entirely:
Subagent/history verification also showed that the tool was not actually executed; the markup was emitted as assistant text.
Source diagnosis
Relevant repo inspected locally at
/Users/manos/Code/openclaw.What the current code does
src/agents/deepseek-text-filter.tscontainscreateDeepSeekTextFilter()which strips DSML wrapper text from visible output.src/agents/openai-transport-stream.tsuses that filter inprocessOpenAICompletionsStream(...), but real tool calls are still created only from nativechoiceDelta.tool_calls.So today the behavior is:
tool_calls, OpenClaw can handle themtoolCallWhy this is likely not just expected model behavior
DeepSeek's own tool-calls docs describe OpenAI-compatible native
message.tool_callsas the intended path:There is also upstream evidence that DeepSeek V4 Pro can intermittently emit tool calls as plain text with
tool_calls: null:So the remaining problem looks like a combination of:
openai-completionsstream pathTested patch candidate
I added a conservative local patch in
src/agents/openai-transport-stream.tsplus regression tests insrc/agents/openai-transport-stream.test.tsthat recover the observed DSML form into synthetictoolCallblocks when:Added test coverage for:
Targeted validation passed:
npx vitest run src/agents/openai-transport-stream.test.ts -t "recovers observed DeepSeek DSML parameter tool call|filters DeepSeek DSML content without disturbing native tool calls|keeps DeepSeek DSML state across native tool-call chunks"Result:
Important caveat
This is currently a source-tree patch candidate, not a fully validated installed-runtime fix yet. The local
openclawCLI I reproed with is still the installed build and did not automatically pick up the edited source tree, so I have not yet re-validated the runtime end-to-end on a build containing the patch.Suggested next step
Take this as a bug + patch candidate:
processOpenAICompletionsStream(...)(or a provider-specific wrapper)Labels / framing suggestion