AI Agent

2026 DeepSeek V4 Multi-Turn 400: Minimal Repro

MacHTML Lab2026.08.18 ~18 min read
2026 DeepSeek V4 Multi-Turn 400: Minimal Repro

A current vLLM reasoning response uses the field reasoning, while the DeepSeek API documents reasoning_content for thinking-mode tool-call replay. That single field mismatch is enough to create a false fix: the first tool call works, but the next request returns HTTP 400. (docs.vllm.ai)

Fastest fix: build two separate raw HTTP reproductions first. For the official DeepSeek API, preserve reasoning_content inside the original assistant message after a tool call. For vLLM, record and replay the field defined by your deployed version. Do not mix reasoning_content and reasoning in one payload or apply a global rename.

This guide is for AI Agent developers who need evidence they can send to a model or framework maintainer. It is also for backend engineers isolating a client, gateway, or inference endpoint failure, and platform teams building regression tests across the official API and a self-hosted vLLM endpoint.

Last updated August 18, 2026. The field rules were checked against the DeepSeek thinking-mode documentation, the DeepSeek chat completion schema, and the current vLLM reasoning output documentation.

Start with the endpoint, not the model name

A model name does not prove which protocol handled your request. Your client may send deepseek-v4 to:

  • The official DeepSeek API.
  • A gateway that rewrites OpenAI-compatible requests.
  • A self-hosted vLLM server.
  • A local compatibility layer that changes response fields.

Those endpoints can expose similar request shapes while enforcing different message contracts. Freeze these values before changing code:

TARGET=official-deepseek
BASE_URL=https://api.deepseek.com
MODEL=<your-confirmed-model-id>
THINKING=enabled

Then create a separate environment for vLLM:

TARGET=vllm
BASE_URL=http://127.0.0.1:8000/v1
MODEL=<model-id-returned-by-your-vllm-server>
REASONING_PARSER=<parser-configured-on-this-server>

Do not infer the target from MODEL. Log the final URL after every redirect or gateway rewrite. Record the deployed vLLM version, parser option, chat template settings, and whether the request uses streaming.

You also need a stable test input. Use one harmless function with deterministic-looking arguments:

{
  "type": "function",
  "function": {
    "name": "lookup_status",
    "description": "Return a fixed status for a test key.",
    "parameters": {
      "type": "object",
      "properties": {
        "key": { "type": "string" }
      },
      "required": ["key"],
      "additionalProperties": false
    }
  }
}

The tool must have no side effects. Your test result should not depend on a database, a live weather service, or a private customer record.

Three hidden costs commonly distort this diagnosis:

  • Serializer loss: a response object is converted to a dictionary and optional fields disappear.
  • Gateway rewriting: a proxy changes reasoning_content, removes content, or converts tool-call objects.
  • State contamination: one test reuses messages produced by another endpoint.

A clean reproduction separates endpoint behavior from application behavior.

Step 1: trigger one tool call with raw HTTP

Start with a first request that contains only the minimum fields needed to trigger a function call. Use the official endpoint and vLLM endpoint separately.

A sanitized official API request can look like this:

curl "$BASE_URL/chat/completions" \
  -H "Authorization: Bearer $DEEPSEEK_API_KEY" \
  -H "Content-Type: application/json" \
  --data-binary @round-1-official.json

Example round-1-official.json:

{
  "model": "<confirmed-model-id>",
  "messages": [
    {
      "role": "user",
      "content": "Check the status for test-key-17."
    }
  ],
  "tools": [
    {
      "type": "function",
      "function": {
        "name": "lookup_status",
        "description": "Return a fixed status for a test key.",
        "parameters": {
          "type": "object",
          "properties": {
            "key": { "type": "string" }
          },
          "required": ["key"],
          "additionalProperties": false
        }
      }
    }
  ],
  "thinking": {
    "type": "enabled"
  }
}

The exact thinking-mode switch must follow the current endpoint schema. Do not copy a parameter from a vLLM deployment into the official request without checking its documentation.

Save the complete response to disk. For review, expose only these fields:

{
  "id": "chatcmpl-redacted",
  "message": {
    "role": "assistant",
    "content": null,
    "reasoning_content": "[redacted]",
    "tool_calls": [
      {
        "id": "call-redacted",
        "type": "function",
        "function": {
          "name": "lookup_status",
          "arguments": "{\"key\":\"test-key-17\"}"
        }
      }
    ]
  },
  "finish_reason": "tool_calls"
}

The DeepSeek schema defines reasoning_content as an assistant-message field and tool_calls as the generated function calls. The tool-call ID is not decorative. You need it when appending the tool result. (api-docs.deepseek.com)

For vLLM, keep the same user message and tool definition, but send the request to the vLLM base URL. Current vLLM reasoning output documentation uses reasoning, not reasoning_content, and states that the older name was replaced. It also documents that tool calling parses functions from the content path rather than treating reasoning text as a function-call channel. (docs.vllm.ai)

Do not rewrite the vLLM response into an official DeepSeek response while you are still collecting evidence. Store the raw response first.

Step 2: preserve the assistant message without preserving secrets

The next request must contain an assistant message reconstructed from the first response. This is where most multi-turn 400 failures begin.

For the official DeepSeek tool loop, the assistant message should preserve the fields required by the official contract:

{
  "role": "assistant",
  "content": null,
  "reasoning_content": "[redacted-original-reasoning]",
  "tool_calls": [
    {
      "id": "call-redacted",
      "type": "function",
      "function": {
        "name": "lookup_status",
        "arguments": "{\"key\":\"test-key-17\"}"
      }
    }
  ]
}

The placeholder above is for documentation only. In the actual replay, use the exact original reasoning value after applying your approved secret-handling policy. Do not invent replacement reasoning text. Do not hash it and assume the API will accept the hash. Do not move it to the tool message.

DeepSeek’s official thinking-mode guide specifically states that when a turn performs tool calls, the generated reasoning_content must be fully passed back in all subsequent requests. The same guide shows the assistant response as the source of the fields needed to continue the tool loop. (api-docs.deepseek.com)

Add assertions before serialization:

assert assistant.role == "assistant"
assert assistant.tool_calls is not empty
assert every tool_call.id is present
assert reasoning field required by target is present
assert message order is user -> assistant -> tool
assert target base URL matches the test case

Run the assertions both before and after JSON serialization. This catches a common boundary failure where a typed response object contains a field, but the JSON encoder omits it because the field is marked optional or unknown.

Important: Logs, redaction middleware, and object conversion are evidence about your client pipeline. They are not evidence that the endpoint accepts a different field contract.

Keep two files:

raw-round-1-response.json
sanitized-round-1-response.json

The first is access-controlled. The second is shareable with maintainers. Never include API keys, full private prompts, complete reasoning text, personal data, or real tool output in the shareable package.

Step 3: append the tool result, then create paired requests

Now append the tool message. Its tool_call_id must match the ID emitted by the assistant message.

{
  "role": "tool",
  "tool_call_id": "call-redacted",
  "content": "{\"status\":\"ok\",\"source\":\"fixture\"}"
}

The complete official replay skeleton is:

{
  "model": "<confirmed-model-id>",
  "messages": [
    {
      "role": "user",
      "content": "Check the status for test-key-17."
    },
    {
      "role": "assistant",
      "content": null,
      "reasoning_content": "[redacted-original-reasoning]",
      "tool_calls": [
        {
          "id": "call-redacted",
          "type": "function",
          "function": {
            "name": "lookup_status",
            "arguments": "{\"key\":\"test-key-17\"}"
          }
        }
      ]
    },
    {
      "role": "tool",
      "tool_call_id": "call-redacted",
      "content": "{\"status\":\"ok\",\"source\":\"fixture\"}"
    }
  ],
  "tools": [
    {
      "type": "function",
      "function": {
        "name": "lookup_status",
        "description": "Return a fixed status for a test key.",
        "parameters": {
          "type": "object",
          "properties": {
            "key": { "type": "string" }
          },
          "required": ["key"],
          "additionalProperties": false
        }
      }
    }
  ],
  "thinking": {
    "type": "enabled"
  }
}

Create two follow-up payloads from the same base object.

Failure sample

Remove the official API’s required reasoning_content from the assistant message. Send the request without changing the URL, model, tool definition, or tool result.

Record:

HTTP status
response body
request target
serialized messages
assistant field names
tool_call_id values

The official documentation identifies a missing replayed reasoning_content value as a cause of HTTP 400 in thinking-mode tool calls. Treat that as the confirmed official condition. Do not generalize it to every OpenAI-compatible endpoint. (api-docs.deepseek.com)

Success sample

Restore the original reasoning_content field in the same assistant message. Send the request again. A successful response proves that this message contract is accepted for this endpoint and deployment state. It does not prove that streaming, a second tool call, or a different gateway will behave identically.

For the vLLM test, do not automatically paste reasoning into a DeepSeek API request. First determine what your deployed vLLM server accepts as input. Current vLLM documentation confirms the output rename from reasoning_content to reasoning, but input compatibility can depend on the server version, protocol model, chat template, and adapter layer. (docs.vllm.ai)

Official API versus vLLM: compare the contract at the boundary

Use this comparison as a debugging rule, not as a universal field-substitution table.

Official DeepSeek API

  • Confirm the real official base URL.
  • Read the current thinking-mode and chat-completion schemas.
  • For a tool-call turn, preserve the original reasoning_content in the assistant message.
  • Preserve tool_calls, function name, serialized arguments, and call IDs.
  • Append the matching tool message after the assistant message.
  • Treat a missing reasoning field as a confirmed candidate for the documented 400 condition.
  • Do not add reasoning merely because another endpoint emits it.

vLLM

  • Confirm the deployed vLLM version and configured parser.
  • Record the actual response fields from /v1/chat/completions.
  • Current vLLM reasoning output documentation exposes reasoning and identifies reasoning_content as the old name.
  • Confirm whether the selected DeepSeek parser and chat template support the tool loop you are testing.
  • Check whether reasoning is enabled through parser or chat-template settings.
  • Test input replay behavior against that deployment instead of assuming compatibility with the official API.
  • Keep the vLLM payload and official payload in separate fixtures.

The current vLLM API reference describes a DeepSeek V4 parser that combines <think> reasoning and DSML tool-call parsing in one state machine. That is evidence about the parser implementation, not proof that its response fields can be sent unchanged to the official DeepSeek API. (docs.vllm.ai)

A safe internal design uses a neutral message model:

InternalAssistantMessage
- content
- reasoning_trace
- tool_calls
- endpoint_origin
- raw_field_map

At the outbound boundary, map only in one direction:

official API -> internal model -> official API
vLLM -> internal model -> vLLM

If you must bridge endpoints, make the mapping explicit:

if target == "official-deepseek":
    emit reasoning_content from the official-compatible source
elif target == "vllm":
    emit the field accepted by the deployed vLLM contract
else:
    fail closed and require a new protocol adapter

A global search-and-replace from reasoning_content to reasoning is unsafe. It can repair a vLLM response parser while breaking the official API replay path.

FAQ: the four failure patterns developers usually miss

Why did round one pass but round two fail?

Round one tests generation. Round two tests message replay. The endpoint can generate a valid tool call while your client loses the assistant’s reasoning field during storage, redaction, serialization, or gateway forwarding. Compare the raw first response with the exact second request body. The difference between those two artifacts is more useful than the framework exception alone.

Where does the official reasoning_content belong?

Place it on the assistant message that also contains the original tool_calls. It does not belong on the tool result, the next user message, or the request root. The tool result must carry the matching tool_call_id. Preserve message order and avoid replacing the original reasoning value with a summary.

Can vLLM reasoning be copied directly into the official API?

Not as a default rule. vLLM’s current documentation uses reasoning, while the official DeepSeek tool-loop contract refers to reasoning_content. These are endpoint-specific fields until your adapter proves otherwise through a documented schema and a raw HTTP test. Use an explicit conversion layer instead of changing historical messages in place.

What is the smallest SDK-free reproduction?

Use one endpoint, one model ID, one user message, one side-effect-free function, and one fixture tool result. Capture the first response, rebuild the assistant message, then send one request with the required field removed and one with the field restored. Keep the request headers, URL, tools, and message text unchanged between the paired cases.

Step 4: turn the reproduction into a regression case

Once you identify the mismatch, stop testing through the full Agent framework. Keep the raw HTTP test as the smallest contract test.

Cover these cases:

  • No tool call and no reasoning replay.
  • One assistant tool call followed by one tool result.
  • Two consecutive tool calls in the same conversation.
  • A failed replay with the endpoint-required reasoning field removed.
  • A successful replay with the original field restored.
  • Switching from the official API fixture to the vLLM fixture.
  • Switching back without reusing messages produced by the other endpoint.
  • Streaming response capture, if production uses streaming.

Do not assert only status == 200. Assert the structure:

assert response.id is present
assert assistant.tool_calls are valid
assert every tool_call.id is unique within the turn
assert every tool result references an existing call ID
assert role order is valid
assert target base URL is expected
assert endpoint-specific reasoning field is present or absent as required
assert error fixture remains a stable 400 candidate

Use a fixed directory layout:

repro/
  official/
    round-1.json
    round-2-missing-reasoning.json
    round-2-correct.json
  vllm/
    round-1.json
    round-2-deployed-contract.json
  fixtures/
    tool-result.json
  README.md

In README.md, record the test date, endpoint URL class, model identifier, vLLM version if applicable, parser configuration, request mode, and redaction rules. The date matters because DeepSeek and vLLM can change field behavior or parser defaults.

A repair is complete only when:

  • The official API failure sample fails for the expected contract reason.
  • The official API corrected sample passes.
  • The vLLM sample passes against the deployed protocol.
  • A second tool-call replay does not silently drop fields.
  • Switching endpoints creates a deliberate adapter transition.
  • The sanitized reproduction can be shared without credentials or private reasoning text.

A decision checklist before you change production code

  • [ ] Confirm the final base URL from the actual HTTP request.
  • [ ] Record the model ID returned or accepted by that endpoint.
  • [ ] Freeze one harmless function definition.
  • [ ] Save the complete first response before redaction.
  • [ ] Extract content, the endpoint-specific reasoning field, tool_calls, finish_reason, and the response ID.
  • [ ] Rebuild the assistant message without moving fields between roles.
  • [ ] Verify every tool_call_id before adding the tool result.
  • [ ] Send a missing-field failure sample.
  • [ ] Send a corrected success sample.
  • [ ] Run the same message skeleton against the second endpoint separately.
  • [ ] Confirm the vLLM version, parser, and chat-template reasoning settings.
  • [ ] Add assertions for message order, field presence, call IDs, and base URL.
  • [ ] Keep separate fixtures for the official API and vLLM.
  • [ ] Re-run the test after SDK, gateway, parser, or model changes.
  • [ ] Store a sanitized reproduction package for maintainers.

If your local machine cannot keep two clean endpoints, two message histories, and repeatable tool fixtures available for every framework upgrade, a temporary isolated MacHTML cloud Mac environment can be useful for this kind of regression work. A local Windows or Linux setup may be cheaper for a long-running self-hosted service, but it often leaves endpoint state, shell configuration, proxy rules, and cached environments mixed together. A shared developer laptop also makes it harder to reproduce the exact same HTTP capture after a rollback. Renting a MacHTML environment is the better fit when you need a short-lived, repeatable test host for validating an Agent fix, comparing endpoint adapters, or replaying a sanitized script before merging it.

For environment access and operating details, review the MacHTML console and the MacHTML help center. If you need persistent heavy inference, dedicated physical peripherals, or a continuously running production endpoint, buying and operating your own hardware may still be the more appropriate choice.

FAQ

Why can the first DeepSeek V4 tool call succeed while the next request returns 400?+
The first request only proves that the endpoint can emit a tool call. The failure usually appears when your client replays the assistant message and drops, renames, or filters a required reasoning field. In DeepSeek thinking-mode tool loops, the official API requires the original reasoning_content to be sent in later requests. A gateway or serializer can break this contract between rounds.
Where should reasoning_content be placed in the next DeepSeek V4 request?+
Keep it inside the same assistant message that contains the original tool_calls. Do not move it into the tool message, a new user message, or a top-level request field. Reconstruct the assistant message from the original response, preserving content, reasoning_content, tool_calls, and the call IDs before appending the matching tool result.
Can vLLM reasoning be sent directly back to the DeepSeek API?+
No. Treat that as an unverified protocol conversion, not a safe rename. Current vLLM documentation exposes reasoning and notes that reasoning_content is its older name, while the DeepSeek API documents its own reasoning_content contract for tool-call replay. Capture each endpoint's response schema, normalize internally, and map fields only at the outbound boundary.
How can you reproduce the DeepSeek V4 multi-turn tool error without an SDK?+
Use raw HTTP against one fixed base URL, one model identifier, one harmless tool, and a short message sequence. Save the first response, rebuild the assistant message, append the tool result using the returned tool_call_id, and send two follow-up payloads: one with the reasoning field removed and one that follows the endpoint's documented contract. Compare status, error body, and serialized messages.

Reproduce Multi-Turn 400 Errors on a Dedicated Mac

Deploy a dedicated Mac mini M4 to test DeepSeek V4 request flows in a consistent remote environment. Run official API and vLLM reproductions without changing your local development machine. Use SSH and remote desktop access to execute curl cases, inspect adapters, and verify endpoint behavior. Choose a flexible MacHTML rental period and keep a reliable environment ready for regression testing.

Rent a cloud Mac mini
Apple Silicon cloud Mac