DeepSeek Harness MALFORMED_RESPONSE: invalid tool JSON fix

TroubleshootingPublished 2026-10-20Author: DeepSeek Plugin Market
DeepSeek HarnessDSHMALFORMED_RESPONSEtool callJSONretry policyllm-deepseek
DeepSeek Harness aborts the whole turn with MALFORMED_RESPONSE when one streamed tool-call has bad JSON. Three ways to retry or isolate the bad call.

Models occasionally emit invalid JSON for tool arguments: an unquoted value, a bare array, a trailing comma; the sensible behavior is to drop that one call and let the model resend it, but DeepSeek Harness (DSH) parses everything at the end of the stream and fails the whole turn with MALFORMED_RESPONSE. A successful ops_bash in the same step, the file contents already read, and the user's wait are all discarded. This guide only covers how to limit the damage after one bad call.

Confirm the DeepSeek Harness invalid JSON scenario

Bottom line: if turn/end carries DeepSeek Messages stream: tool input is invalid JSON and the same step contains more than one tool-call, you are in the right place. A single call failing with no siblings is a different problem. A typical log:

{"type":"turn/end","seq":19,"data":{"turn":1,
 "reason":{"kind":"error","error":{
   "message":"DeepSeek Messages stream: tool input is invalid JSON",
   "code":"MALFORMED_RESPONSE"}}}}

Scroll one step back and you will usually see two or more tool-calls. Confirm with these steps:

  1. Search the turn/end error for tool input is invalid JSON; expected: code: MALFORMED_RESPONSE.
  2. Re-read the step/* event for the same step; expected: two or more tool-calls, not one.
  3. Compare the argument shapes; expected: one has an unquoted value, a bare array, or a trailing comma.

After the turn fails, even the valid result is not fed back. For other adapter faults, see LLM adapter development.

Why one bad call takes down the DeepSeek Harness turn

Bottom line: parsing happens in the message_stop branch of the stream translator and throws an LlmError; that path has no partial-success exit. According to the source review in the discussion, the logic in llm-deepseek/src/translate.ts is equivalent to:

ts
if (reason.kind !== "max-tokens") for (const { content } of blocks.values()) {
  if (content.type !== "tool-call") continue
  let parsed
  try {
    parsed = JSON.parse(content.arguments)
  } catch (_invalidProviderToolJson) {
    return malformed("tool input is invalid JSON")
  }
  object(parsed)
}

malformed() at :25-26 throws new LlmError(..., "MALFORMED_RESPONSE"). Note that :148 requires every block to be closed first, so the order is: run what can run, validate everything at the end, and discard the turn when one block fails. The catch parameter name _invalidProviderToolJson shows the input was anticipated; the choice was to fail the turn.

Three DeepSeek Harness fixes for bad tool JSON

Bottom line: add the code to the retry allowlist for first aid, install a guard plugin if you cannot touch config, and change the closing check if you can ship a core patch.

Fix 1: add MALFORMED_RESPONSE to the retry allowlist

This is the smallest change and the best first aid, at the cost of one extra full request's tokens. Retries are handled by the retry policy, so no model code changes.

  1. Open the current profile's cordis.patch.yml and identify the llm-deepseek-account row or whichever provider you use; expected: one target row is identified.
  2. Add retryableCodes to that row and append MALFORMED_RESPONSE, keeping the existing codes; expected: the configuration parses.
  3. Restart DSH and send a request that triggers multiple parallel tool calls; expected: a failed request can be retried.
  4. Check the log; expected: a retry record followed by a successful turn.
  5. If two attempts both fail, retries stop; expected: no infinite loop, so move to Fix 2 or 3.

This fix retries the whole request, not one call, so tools with side effects may run twice. If the bad JSON is deterministic for a prompt, retries only waste time; change the prompt or the model.

Fix 2: install a guard plugin at the stream layer

When you do not want to edit profile configuration, @argszero/dsh-malformed-tool-json-guard intercepts the same error in the llm/stream waterfall and survives upgrades that reset config. Steps:

  1. Install the plugin through the market; expected: it is active in the plugin list.
  2. Open its configuration and ensure the intercepted code list includes MALFORMED_RESPONSE; set the retry cap to 1 or 2; expected: the settings save.
  3. Replay a failing session; expected: the first failure is followed by an automatic resend that succeeds.
  4. If the plugin conflicts with an existing provider retry setting, disable one of the two layers before restarting; expected: only one retry layer remains.

Fix 3: isolate the bad call in the closing check

This is the only approach that returns valid results and feeds the broken call back individually. It suits core changes or a maintained fork. The goal is to reduce the failure unit from the turn to a single call.

  1. In the JSON.parse failure branch of translate.ts, stop returning malformed(...); mark the block with invalidArguments: true instead; expected: a parse failure no longer throws immediately.
  2. At the end of message_stop, convert marked blocks into a tool result that says the arguments were invalid and includes the tool name and a snippet; expected: the bad call has a concrete response.
  3. Teach the tool layer to recognize that result: do not execute the tool, only pass the message back in the next model request; expected: the tool is not executed by mistake.
  4. Execute the unmarked blocks normally and emit usage and finish as usual; expected: the turn ends as completed.
  5. Add two tests, one bad call alone and one good call beside a bad one; expected: the good result exists, the bad call is fed back, and the turn succeeds.

Verify DSH fixes and avoid regressions

Bottom line: one normal request and one deliberately broken request must both pass through the retry and isolation paths without valid calls being misclassified. Verify:

  1. Construct a request with two tool calls in one step and deliberately unquote one value; expected: MALFORMED_RESPONSE reproduces reliably.
  2. With Fix 1 or 2, confirm a retry appears; expected: the turn ends as completed.
  3. With Fix 3, confirm the valid result is retained and the bad call receives an invalid-argument response; expected: the turn still completes instead of erroring.
  4. Send a normal request afterwards; expected: valid calls are not misclassified as malformed.

Avoid these traps:

  • Never set MALFORMED_RESPONSE to unlimited retries; a deterministic prompt will loop and burn tokens.
  • Add idempotency to side-effecting tools; a whole-turn retry can run git commit or rm twice.
  • The error currently lacks tool name and byte offset; Fix 3 can add both, which makes future diagnosis much faster.
  • No official fixed version is listed; 0.2.1-alpha.4 and rc.2 both reproduce, so treat release notes as the source of truth and do not assume an old DSH plugin behavior is fixed.

DeepSeek Harness tool JSON FAQ

Q: Why does DeepSeek Harness fail the whole turn on tool input is invalid JSON? A: DeepSeek Harness fails the whole turn because validation runs once after message_stop. Any tool-call whose arguments fail JSON.parse throws an LlmError, and results from calls that already ran never reach the success branch of turn/end.

Q: Why is MALFORMED_RESPONSE never retried in DeepSeek Harness when a resend usually succeeds? A: DeepSeek Harness leaves MALFORMED_RESPONSE out of the default retryable codes, so it is treated as deterministic. The trigger is often a transient JSON syntax glitch, and adding the code to the allowlist or installing a guard plugin turns a hard failure into one automatic retry.

Q: How can DeepSeek Harness drop only the broken tool call instead of the whole turn? A: DeepSeek Harness can isolate the bad call by changing the closing branch in translate.ts: mark the block as having invalid arguments instead of throwing. The tool layer then feeds an invalid-argument result back to the model while valid calls execute normally.

Q: How do I locate the broken tool call when DeepSeek Harness omits its name? A: DeepSeek Harness omits the tool name, so inspect the tool-call blocks in the last step before the turn/end error and compare their argument shapes. Look for unquoted values, bare arrays, or a trailing comma; add the tool name and parse position for a permanent fix.

FAQ

Why does DeepSeek Harness fail the whole turn on tool input is invalid JSON?

DeepSeek Harness fails the whole turn because validation runs once after message_stop. Any tool-call whose arguments fail JSON.parse throws an LlmError, and results from calls that already ran never reach the success branch of turn/end.

Why is MALFORMED_RESPONSE never retried in DeepSeek Harness when a resend usually succeeds?

DeepSeek Harness leaves MALFORMED_RESPONSE out of the default retryable codes, so it is treated as deterministic. The trigger is often a transient JSON syntax glitch, and adding the code to the allowlist or installing a guard plugin turns a hard failure into one automatic retry.

How can DeepSeek Harness drop only the broken tool call instead of the whole turn?

DeepSeek Harness can isolate the bad call by changing the closing branch in translate.ts: mark the block as having invalid arguments instead of throwing. The tool layer then feeds an invalid-argument result back to the model while valid calls execute normally.

How do I locate the broken tool call when DeepSeek Harness omits its name?

DeepSeek Harness omits the tool name, so inspect the tool-call blocks in the last step before the turn/end error and compare their argument shapes. Look for unquoted values, bare arrays, or a trailing comma; add the tool name and parse position for a permanent fix.

Related Terms

MALFORMED_RESPONSE
MALFORMED_RESPONSE is the DeepSeek Harness error code for an upstream response that cannot be parsed. Broken JSON in streamed tool arguments is the most common cause, and it is not in the default retry allowlist.— https://github.com/deepseek-ai/deepseek-harness/discussions/10195
message_stop
message_stop is the closing event of a streamed response, indicating that all content blocks have ended. DeepSeek Harness validates tool-call arguments after this event, not as each delta arrives.— https://github.com/deepseek-ai/deepseek-harness/discussions/10195
retryableCodes
retryableCodes is the set of error codes that DeepSeek Harness may retry automatically. It includes network and rate-limit errors by default but not MALFORMED_RESPONSE; a profile can extend it per provider.— https://github.com/deepseek-ai/deepseek-harness/discussions/10195

Sources