May 11th, 2026

Laravel AI: Fix DeepSeek 400 Bad Request on Multi-Turn Tool Calls

Laravel AI: Fix DeepSeek 400 Bad Request on Multi-Turn Tool Calls
Sponsored by
Table of Contents

@k1rana merged PR #534 into laravel/ai, fixing a 400 Bad Request error from the DeepSeek API that surfaced whenever a multi-turn conversation included tool calls. The change resolves issue #533.


Why DeepSeek Rejects the Request

DeepSeek requires that every AssistantMessage carrying tool_calls also includes the exact reasoning_content from that same turn in all subsequent requests. If reasoning_content is absent, the API returns a 400 regardless of whether the rest of the payload is well-formed.

The previous implementation had two separate failure paths. First, during recursive internal tool-call loops, reasoning_content was never captured from the response and therefore never replayed back to the API on the next turn. Second, when historical messages were reconstructed from a database where reasoning_content was never persisted, the API rejected the request because tool_calls were present without their accompanying reasoning context.


Capturing and Replaying reasoning_content

ParsesTextResponses and HandlesTextStreaming now capture reasoning_content into providerContentBlocks alongside the rest of the response data. When the agent enters a recursive tool-call loop, those blocks are included in the next request to DeepSeek, satisfying the API contract.

The relevant shape being stored looks like this:

1$providerContentBlocks[] = [
2 'type' => 'reasoning',
3 'reasoning_content' => $response->reasoning_content,
4];

On the next recursive call, those blocks are mapped back into the outgoing AssistantMessage, so DeepSeek receives exactly what it produced on the prior turn.


Handling Historical Messages Without reasoning_content

Persisted conversations are a harder case. If reasoning_content was never stored alongside a historical AssistantMessage, there is no way to reconstruct it. Sending tool_calls without it would still trigger a 400.

MapsMessages@mapMessagesToChat now detects this condition and handles it cleanly. When an assistant message has tool_calls but no reasoning_content, the mapper strips tool_calls from the payload and skips the paired ToolResultMessage. This prevents orphaned tool outputs from appearing in the conversation while keeping the assistant's text response intact, so the conversational context is preserved as much as possible.

1// Inside mapMessagesToChat, when processing a historical AssistantMessage
2if (!empty($message->toolCalls) && empty($message->providerMeta['reasoning_content'])) {
3 // Strip tool_calls and skip the paired ToolResultMessage
4 $mapped[] = [
5 'role' => 'assistant',
6 'content' => $message->content ?? '',
7 ];
8 $skipNext = true;
9 continue;
10}

Who Should Care

Any project using laravel/ai with a DeepSeek provider and tools configured will hit this bug as soon as a conversation reaches a second turn after a tool call. That includes both stateless recursive loops and stateful conversations stored in a database. Upgrading picks up the fix with no configuration changes required.

If you enjoyed this article, please consider supporting our work for as low as $5 / month.

Sponsor
Marian Pop

Written by

Marian Pop

Writing and maintaining @LaravelMagazine. Host of "The Laravel Magazine Podcast". Pronouns: vi/vim.

Comments

Stay Updated

Subscribe to our newsletter

Get latest news, tutorials, community articles and podcast episodes delivered to your inbox.

Weekly articles
We send a new issue of the newsletter every week on Friday.
No spam
We'll never share your email address and you can opt out at any time.