Laravel AI SDK v1.4.0 changes what happens when a tool throws an exception. The model now receives the failure as a tool result and the run continues, so the agent can explain the problem or try something else. The release also lets faked agents resume paused tool approvals, adds the raw HTTP response to five more response types, and reports OpenAI billing errors with the correct exception.
- A tool that throws no longer ends the agent run
- Agent fakes run approved tools and store their results
rawon classification, embeddings, transcription, image, and reranking responses- OpenAI quota and credit errors throw
InsufficientCreditsExceptioninstead ofRateLimitedException - Provider stream errors broadcast under a fixed
errorname - Conversation title generation fires the usual agent events
- Fixes for Anthropic and OpenAI-compatible tool calls
What's New
Agents Recover From Tool Failures
Before this release, an exception thrown inside a tool ended the run and fired AgentFailed. Now the SDK catches the exception and sends its message back to the model as a failed tool result. If a RefundOrder tool throws "Order 1234 can no longer be refunded.", the model receives:
The tool call failed: Order 1234 can no longer be refunded.
The model can then tell the user what went wrong or call a different tool. The exception still goes to your exception handler through report(), ToolFailed still fires, and a streamed run emits the failure as a ToolResult event with successful set to false. An approved tool that threw was already handled this way, so every tool now behaves the same.
Only the tool's own exception is recovered. An exception thrown by an event listener still fails the run.
PR: #1191 by @pascalbaljet
For examples and caveats, see Handle Tool Exceptions in Laravel AI SDK Agents.
Test Tool Approval Flows With Agent Fakes
Human-in-the-loop tool approval pauses a run until someone approves or rejects a tool call. Under Agent::fake(), resuming that run skipped the tool entirely: an approved tool never ran, and no result was streamed or stored. You could not test an approval flow from start to finish.
A faked agent now resumes a paused call the same way a provider does. An approved tool runs and its result is stored, and a rejected tool gets a denied result, before the fake returns its next response:
SupportAgent::fake([ new ToolCall('call-1', 'SendEmail', ['to' => 'john@example.com']), 'The email is on its way.', ]); $paused = (new SupportAgent)->forUser($user)->prompt('Email John the release notes'); $response = (new SupportAgent) ->continue($paused->conversationId, as: $user) ->prompt(Decisions::from(['call-1' => Decision::approve()])); $response->toolResults[0]->result; // "Email sent to john@example.com." $response->text; // "The email is on its way."
PR: #1190 by @pascalbaljet
For full test examples covering approve, reject, and edit decisions, see Test Tool Approval Flows in the Laravel AI SDK.
Raw HTTP Responses on More Response Types
Text responses have had a raw property holding the provider's HTTP response since 0.10.3. Classification, embeddings, transcription, image, and reranking responses now have it too, so you can read provider fields the SDK does not map, such as the usage.cost value OpenRouter returns:
$response = Classification::of($ticket) ->questions($questions) ->classify(provider: 'openrouter'); $cost = $response->raw->json('usage.cost');
The property is null after serialization. On embeddings, it is also null when every vector came from the cache.
PRs: #1178, #1179, #1180, #1181, #1182 by @pushpak1300
OpenAI Billing Errors Throw InsufficientCreditsException
OpenAI returns a 429 status for both rate limits and billing problems such as insufficient_quota and credit_balance_exhausted. The SDK treated every 429 as a rate limit, so billing errors threw RateLimitedException and could be retried when the response had a short Retry-After header.
A 429 with one of OpenAI's billing codes now throws InsufficientCreditsException and is not retried. Other 429 responses still throw RateLimitedException. Failover is unchanged, since both exceptions are failoverable.
PR: #1174 by @grzegorczykp
Stream Errors Broadcast Under a Fixed Name
Broadcast stream events use their type as the event name, such as text_delta or tool_call. A provider error used the provider's error code instead, such as 403 or PERMISSION_DENIED, so a frontend could not know which name to listen for. Provider errors now broadcast as error. The payload's type still carries the provider's code, and job failures still broadcast as stream_failed.
Conversation Title Generation Fires Agent Events
Generating a conversation title makes a second model call, but it fired no events, so its token usage and failures did not show up in your agent run events. The title call now runs as an agent run on the turn's provider and cheapest model. Its agent is an AnonymousAgent, and its parentInvocationId points at the turn that triggered it. A failed title call still falls back to the prompt text and never fails the turn.
PR: #1203 by @pushpak1300
Other Fixes
- Anthropic tool calls run when the message ends with
end_turn(#1188), and streamed text is joined acrosspause_turncontinuations (#1186) - OpenAI-compatible tool calls run when sent without an index or finished with
stop(#1192) - OpenAI batch embeddings keep the input order (#1172), and custom voices are sent as an object (#1212)
- Stored message IDs are copied into the
RUN_FINISHEDmetadata, where@ag-ui/client1.x keeps them (#1165) - A resumed turn's content is rebuilt from its merged steps (#1162)
- Mistral models are updated to their latest versions (#1173)
Upgrade Notes
A tool exception no longer fails the run or fires AgentFailed. If your code depends on the old behavior, rethrow from a ToolFailed listener, since a listener exception still ends the run:
use Illuminate\Support\Facades\Event; use Laravel\Ai\Events\ToolFailed; Event::listen(ToolFailed::class, fn (ToolFailed $event) => throw $event->exception);
Tests that resume an approval under Agent::fake() now run the approved tool. A rejection without a reason now ends the turn, as it does with a real provider, so a test that expects a reply after rejecting should pass one, such as Decision::reject('Not now.').
Code that catches RateLimitedException to handle OpenAI quota or billing errors needs to catch InsufficientCreditsException instead.
Frontends that listened for a provider error by its code need to listen for error.
Listeners on PromptingAgent, AgentPrompted, AgentFailed, and the step events now also receive title generation runs. Check parentInvocationId if you need to tell them apart from a user's turn.
Upgrade with composer update laravel/ai, and see the official documentation for configuration. Our Laravel AI SDK 1.1 post covers the previous minor release.
References