DeepSeek V4 Second-Round 400: How to Read Request Logs

The first tool call succeeds, but the next DeepSeek V4 request returns 400 after the tool result is added.
Fastest fix: do not retry, scale up, or replace fields globally. Compare the same session at four points: model response, in-memory messages, persisted history, and final outbound request. DeepSeek’s official API requires reasoning_content to survive tool-call turns, while current vLLM documentation centers on reasoning; mixed deployments need an explicit adapter.
This guide is for:
- Agent developers maintaining multi-turn
function callingloops. - Backend engineers checking SDK serialization, API gateways, databases, and queues.
- Platform teams switching between the DeepSeek API and a vLLM endpoint.
Last updated August 15, 2026. Field behavior was checked against the current DeepSeek thinking-mode and tool-call documentation, plus the current vLLM reasoning and OpenAI-compatible protocol documentation.
Start with the failure signature, not the retry count
A common log sequence looks like this:
request=turn-1
response=200
assistant.tool_calls=[call_abc]
assistant.reasoning_content=<present>
request=turn-2
messages += tool result
response=400
error=invalid request
That pattern narrows the search immediately.
If the first request generated a valid tool call, the tool definition was accepted at least once. The second request is where the application usually reconstructs the conversation. That reconstruction may remove an unknown field, rename a field, reorder messages, or serialize an empty value differently.
The first artifact to preserve is not the full prompt. It is a small, redacted session containing:
- The first user message.
- The complete assistant message returned with
tool_calls. - The matching
toolmessage. - The exact second request sent to the target endpoint.
- The HTTP status and response body.
Keep message indexes, roles, field names, tool-call IDs, and endpoint type. Remove API keys, full reasoning text, personal data, and real tool output.
The key comparison is structural:
assistant fields after model response:
["role", "content", "reasoning_content", "tool_calls"]
assistant fields in final request:
["role", "content", "tool_calls"]
If reasoning_content disappears between those two lines, the failure is a history or serialization problem. Increasing retries will not repair it.
DeepSeek’s documented thinking-mode tool-call flow returns an assistant message containing content, reasoning_content, and tool_calls, then appends the related tool message before the next completion request. (DeepSeek thinking-mode documentation)
Why does the first tool call work but the second request fail?
The most likely explanation is that the first request tests the tool schema, while the second request tests the entire reconstructed message chain.
That creates several hidden failure points:
- Field loss: the Agent stores only
contentandtool_calls. - Field renaming: the response uses
reasoning, but the DeepSeek API expectsreasoning_content. - Unknown-field filtering: a gateway or SDK model drops fields outside its allowlist.
- Object rebuilding: a database consumer creates a new assistant object and copies only known OpenAI-compatible properties.
- Streaming merge errors: reasoning deltas and content deltas are merged into one string, or one of them is discarded.
- Message-chain errors: the
tool_call_idno longer matches the assistant tool call. - Endpoint mismatch: the client sends a vLLM-shaped response object directly to the DeepSeek API.
The failure can also come from parameters unrelated to reasoning. DeepSeek’s thinking mode documentation lists restrictions on several sampling parameters, and its tool-call documentation defines the required structure for tool responses. A field mismatch should therefore be proven from the request diff or error body, not assumed from the status code alone. (DeepSeek thinking-mode documentation)
Decision conditions for the first diagnosis
- If the final request has no reasoning field: inspect the Agent serializer, database schema, and message mapper.
- If the final request has
reasoning_contentand targets the DeepSeek API: inspect message order, tool IDs, assistant content, and unsupported parameters. - If the final request has
reasoningand targets the DeepSeek API: add an outbound mapping layer. - If the final request has
reasoning_contentand targets vLLM: verify the deployed vLLM version and protocol behavior instead of assuming old compatibility. - If the same payload succeeds when sent directly but fails through the gateway: the gateway or consumer is changing the request.
- If direct requests fail on both endpoints: reduce the session to one tool call and validate the endpoint contract first.
This branch prevents two expensive mistakes: replacing a working model because the history was corrupted, and adding both fields everywhere without understanding which endpoint consumes which field.
DeepSeek V4 second-round 400: the missing-field path
DeepSeek’s official thinking-mode rule is specific. When an assistant turn performs a tool call, its reasoning_content must be passed back in subsequent requests. If the field is not returned correctly, the API can respond with HTTP 400. This is different from an ordinary reasoning turn without tools, where earlier reasoning content does not need to remain in the conversation context. (DeepSeek thinking-mode documentation)
A minimal assistant record for the DeepSeek API should preserve the relevant structure:
{
"role": "assistant",
"content": "",
"reasoning_content": "<redacted>",
"tool_calls": [
{
"id": "call_abc",
"type": "function",
"function": {
"name": "lookup_status",
"arguments": "{\"id\":\"redacted\"}"
}
}
]
}
The following tool message must point to the same call:
{
"role": "tool",
"tool_call_id": "call_abc",
"content": "<redacted result>"
}
The most common implementation mistake is a narrow persistence model:
saved_message = {
"role": response.role,
"content": response.content,
"tool_calls": response.tool_calls,
}
This looks harmless because many ordinary chat turns only need role and content. It fails when a thinking-mode assistant message used a tool. The storage code has converted a complete model response into an incomplete history object.
Use an explicit allowlist that is endpoint-aware:
assistant_message = {
"role": message["role"],
"content": message.get("content"),
"tool_calls": message.get("tool_calls"),
"reasoning_content": message.get("reasoning_content"),
}
Do not log the reasoning text itself unless policy permits it. Log the field’s presence, type, length, and hash instead:
assert "reasoning_content" in assistant_message
assert isinstance(assistant_message["reasoning_content"], str)
The assertion should run before the second outbound request. It changes a remote 400 into a local, actionable error.
A separate edge case matters here. DeepSeek’s reasoning-model documentation says that reasoning content from ordinary non-tool turns should not be sent back in the next request. Tool-call turns follow a different rule. A generic “always strip reasoning” or “always preserve reasoning” middleware is therefore unsafe. The decision must depend on whether the assistant message contains a tool call. (DeepSeek reasoning-model documentation)
reasoning_content versus vLLM reasoning: where direct reuse breaks
The field names look similar enough to encourage accidental reuse.
The DeepSeek API documents the assistant reasoning field as reasoning_content. Current vLLM reasoning-output documentation presents the primary field as reasoning and notes that reasoning_content was the older name. The documentation also recommends migration because the older name may be removed in the future. (vLLM reasoning outputs documentation)
That means one internal message object should not be sent unchanged to both endpoints.
A safer internal representation is neutral:
internal_message = {
"role": "assistant",
"content": content,
"reasoning": reasoning_text,
"tool_calls": tool_calls,
}
Then map at the boundary:
def to_deepseek_api(message):
output = {
"role": message["role"],
"content": message.get("content"),
"tool_calls": message.get("tool_calls"),
}
if message.get("tool_calls"):
output["reasoning_content"] = message.get("reasoning")
return output
For a vLLM target, the outbound representation should follow the deployed protocol and model template:
def to_vllm(message):
return {
"role": message["role"],
"content": message.get("content"),
"reasoning": message.get("reasoning"),
"tool_calls": message.get("tool_calls"),
}
The adapter must identify the endpoint from configuration, not from a guessed model name. Useful metadata includes:
- Provider: DeepSeek API or vLLM.
- Base URL.
- API path.
- Served model name.
- vLLM version.
- Reasoning parser.
- Tool-call parser.
- Thinking-mode settings.
Current vLLM documentation states that reasoning output can coexist with tool calling, but tool calling is parsed from the content field rather than the reasoning field. This distinction matters when a stream merger incorrectly treats reasoning text as the place where function calls should be extracted. (vLLM reasoning outputs documentation)
Compatibility warning: vLLM’s older-field behavior is version-dependent. Treat
reasoning_contentcompatibility as a deployment fact to verify, not as a permanent contract.
The correct answer to “can vLLM’s reasoning response be sent directly to the DeepSeek API?” is no, not without an adapter. The names, validation rules, and tool-loop expectations are different. The adapter can map reasoning to reasoning_content, but it must also confirm that the assistant message is otherwise valid for the DeepSeek API.
Step 1: compare every hop where a field can disappear
A multi-service Agent usually has more than one serialization boundary:
- SDK response object.
- Agent memory object.
- JSON serializer.
- API gateway request.
- Database or cache record.
- Queue message.
- Consumer reconstruction.
- Final HTTP payload.
Print a field inventory at each hop:
def inspect_message(label, message, index):
print({
"label": label,
"index": index,
"role": message.get("role"),
"fields": sorted(message.keys()),
"has_reasoning_content": "reasoning_content" in message,
"has_reasoning": "reasoning" in message,
"tool_call_ids": [
item.get("id")
for item in message.get("tool_calls", [])
],
})
Do not print the complete reasoning content. The useful evidence is the field set and message index.
Check these failure modes in order:
Unknown-field filtering
Pydantic models, TypeScript schemas, ORM serializers, and gateway validators often remove fields they do not recognize. A field may exist in the SDK object but vanish after conversion to a plain dictionary.
Compare:
SDK object fields
serialized JSON fields
gateway body fields
consumer object fields
final request fields
The first point where the field disappears identifies the repair layer.
Empty-value cleanup
A cleanup function may remove keys whose values are empty strings or null. That can damage the tool-call assistant message if the framework uses an empty content value while placing the actual action in tool_calls.
Do not apply a global rule such as:
payload = {k: v for k, v in payload.items() if v}
It removes meaningful structural values. Clean fields by contract, not by truthiness.
Streaming merge
When streaming is enabled, reasoning and content often arrive in separate deltas. A faulty merger may:
- Append reasoning to
content. - Keep only the final content delta.
- Drop reasoning when a tool-call delta arrives.
- Store tool calls but not their preceding reasoning field.
- Reconstruct the assistant message with a different null policy.
Run one non-streaming reproduction first. If non-streaming succeeds but streaming fails, the problem is in delta accumulation or final object construction, not in the tool schema.
Step 2: validate message order and tool identity
A complete history is not only a set of fields. It is an ordered chain.
For a standard tool loop, the sequence should be equivalent to:
user
assistant with tool_calls
tool with matching tool_call_id
assistant response
The DeepSeek tool-call documentation shows the assistant tool-call message followed by a tool result carrying the matching tool_call_id. (DeepSeek tool-call documentation)
Check every assistant tool call:
for assistant in messages:
for tool_call in assistant.get("tool_calls", []):
call_id = tool_call["id"]
matches = [
item for item in messages
if item.get("role") == "tool"
and item.get("tool_call_id") == call_id
]
assert len(matches) == 1, call_id
Also check that:
- The tool result appears after the assistant tool call.
- The tool result is not inserted before the assistant message.
- A queue consumer has not sorted messages by database timestamp.
- Duplicate tool results were not appended after a timeout retry.
- Parallel tool calls retain their individual IDs.
- The assistant message was not converted to a user message by a generic transcript formatter.
A 400 caused by message order should not be “fixed” by copying reasoning_content into every message. That would hide the chain error and create new incompatibilities.
Step 3: check parameters after the history is proven
Once the field set and message order are correct, inspect extra request parameters.
Thinking-mode requests can have parameter restrictions. DeepSeek’s official documentation identifies unsupported sampling parameters, including temperature, top_p, presence_penalty, and frequency_penalty for thinking mode. The documentation also notes that some compatibility parameters may be accepted but have no effect, which can make a configuration appear valid while behaving differently than expected. (DeepSeek thinking-mode documentation)
Check these values in the final request:
thinking.reasoning_effort.tool_choice.parallel_tool_calls.temperature.top_p.response_format.- Model name.
- Base URL and API path.
Do not assume that a parameter supported by vLLM is supported by the DeepSeek API. vLLM’s current Chat Completion protocol exposes its own reasoning and serving extensions, including reasoning-related request fields and tool settings. That is evidence of a separate endpoint contract, not proof of full wire-level equivalence. (vLLM Chat Completion protocol)
For vLLM, also verify the server startup configuration. Current vLLM documentation uses a reasoning parser and a tool-call parser as separate serving concerns. The correct parser depends on the model and deployment version. (vLLM CLI arguments documentation)
Step 4: replay one session against both runtimes
A repair is complete only when the same minimum session is replayed through separate paths:
- Direct DeepSeek API request.
- Direct vLLM request.
- Full application path through gateway, storage, queue, and Agent loop.
Use the same redacted user message and the same tool schema. Do not reuse the same serialized assistant object without mapping it.
Record:
- Endpoint type.
- Base URL pattern.
- Model identifier.
- vLLM version, if applicable.
- Reasoning field emitted.
- Reasoning field sent.
- Tool-call IDs.
- Message indexes.
- Final HTTP response.
- Diff between the failing and repaired payload.
The replay result should identify a layer:
DeepSeek direct: pass
vLLM direct: pass
Full path: fail
First loss: database consumer
Or:
DeepSeek direct: pass
vLLM direct: pass with reasoning
DeepSeek adapter: fail
First mismatch: reasoning was not mapped to reasoning_content
The repair belongs at that boundary. Do not add both reasoning and reasoning_content to every message as a universal workaround. It increases payload ambiguity and can send a field to an endpoint that rejects or ignores it.
What should stay in the permanent regression test?
Keep one compact fixture for the exact failure signature:
- One user request.
- One assistant tool call.
- One redacted tool result.
- One second model request.
- One expected assistant field set per endpoint.
The test should assert more than HTTP 200:
assert first_response.status_code == 200
assert "tool_calls" in first_response.message
assert second_request.messages[1]["role"] == "assistant"
assert second_request.messages[2]["role"] == "tool"
assert second_request.messages[2]["tool_call_id"] == "call_abc"
For the DeepSeek API path:
assert "reasoning_content" in second_request.messages[1]
For the vLLM path, assert the field required by the tested deployment and parser. Pin the vLLM version in the test environment. Current vLLM documentation explicitly describes protocol behavior that can change across releases, so a successful test on one version is not a permanent compatibility guarantee.
The regression fixture should run after changes to:
- SDK versions.
- Gateway schemas.
- Database message models.
- Stream handling.
- Provider routing.
- vLLM startup flags.
- Model or chat-template configuration.
For deployment notes and operational access, keep the tested endpoint details with the team’s internal runbook rather than inside application logs. ProxyMac’s help center and console access page can be used when the Agent requires a repeatable remote environment for controlled replay.
Why a fixed Mac environment can be better than the current setup
If the current debugging setup is a developer laptop, a short-lived Linux VM, or an improvised cloud instance, it has three recurring weaknesses: runtime drift, inconsistent network behavior, and no stable place to preserve the same client-side test harness. Those differences can make a gateway bug look like a model bug.
A managed Mac environment does not replace the DeepSeek or vLLM adapter. It gives the adapter a repeatable place to run, record, and replay. That is useful when the project also depends on macOS clients, Xcode automation, Apple Silicon testing, or long-running Agent jobs.
For a stable heavy-load inference backend, buying dedicated hardware or using a dedicated server may still be the better choice. For temporary regression work, endpoint comparison, and reproducible macOS-side automation, renting a Mac through ProxyMac can avoid the maintenance cost of rebuilding the environment for every incident. The important decision is to rent for controlled testing and temporary capacity, not to use rental infrastructure as a substitute for fixing the message adapter.
The next debugging action is concrete: preserve one failing session, compare the four payload stages, map reasoning and reasoning_content only at the endpoint boundary, then replay the repaired request against both runtimes before changing model capacity or retry policy.
Debug Your Agent Workflows on a Remote Mac
Rent a dedicated Mac from ProxyMac to reproduce multi-step request failures in a consistent environment.
Use remote Mac access to compare payloads, inspect request logs, and test each conversation turn without changing your local setup.