The Laravel AI SDK returns a typed response object: $response->text, $response->usage, $response->meta, and so on. Those properties cover what every provider has in common, and nothing more. Rate limit headers, the provider's own request id, and payload fields outside the generic shape were all unreachable.
Version 0.10.3, released on August 6, 2026, makes them reachable. @dumbbellcode added a public raw property in #714 that holds the Illuminate\Http\Client\Response from the call the SDK made on your behalf:
$response = (new SupportAgent)->prompt('Summarize this document.'); $response->raw->header('x-ratelimit-remaining-requests');$response->raw->json('id');
It is the same HTTP client response you get back from Http::get(), so header(), json(), and status() all work as usual.
Every Step Keeps Its Own Response
An agent run that calls tools makes more than one request. The provider returns tool calls, the SDK executes them, sends the results back, and repeats until the model stops asking for tools. $response->raw gives you the HTTP response of the final request in that loop, which is the one that produced the text you got back.
Each step keeps its own raw:
foreach ($response->steps as $step) { $step->raw?->header('x-ratelimit-remaining-tokens');}
A five-step run made five requests, and the rate limit budget it consumed is spread across all five headers, not just the last one.
Event listeners get the same object, since AgentPrompted carries the response itself:
use Laravel\Ai\Events\AgentPrompted; Event::listen(AgentPrompted::class, function (AgentPrompted $event) { $remaining = $event->response->raw?->header('x-ratelimit-remaining-requests'); if ($remaining !== null && (int) $remaining < 10) { Log::warning('Provider request budget running low.', [ 'provider' => $event->response->meta->provider, 'remaining' => $remaining, ]); }});
Reading a header at the call site means passing it back to whatever called the agent. A listener keeps the check in one place and applies it to every run.
Correlating a Bad Run With the Provider
When a run produces something wrong and you contact the provider, they ask for the request id from their side. Without raw, the only way to get it was to log the whole request yourself with an HTTP client middleware, which meant capturing prompts you may not want in your logs.
Now you read it from the response you already have:
Log::info('Agent run completed.', [ 'invocation' => $response->invocationId, 'provider_request_id' => $response->raw?->header('request-id'),]);
Header names differ by provider, so check the one you are calling.
When raw Is Null
The property is nullable, so use ?-> just to be safe. Four cases could return null:
- Streamed responses.
$agent->stream()and theAgentStreamedevent always report null, because a streamed response is assembled from stream events rather than from one response body. - Bedrock. The AWS SDK performs the call, so there is no HTTP client response to hand back. Every other HTTP-based provider populates it: Anthropic, OpenAI, Azure OpenAI, DeepSeek, Gemini, Groq, Mistral, Ollama, OpenAI-compatible, OpenRouter, and xAI.
- Serialized responses. The response body is a Guzzle stream, and Guzzle throws a
LogicExceptionif anything tries to serialize one. The SDK response dropsrawin its own__serialize()for that reason, so a response that has been through a queue or a cache comes back withrawset to null. If a queued job needs a header, read it before dispatching and pass the value. - Faked agents, unless the fake supplies one. See the next section.
Faking Responses in Tests
Rate limit handling only runs when a provider is close to cutting you off, which makes it difficult to test. Fake responses can carry a raw of their own through withRawResponse():
use GuzzleHttp\Psr7\Response as Psr7Response;use Illuminate\Http\Client\Response;use Laravel\Ai\Responses\TextResponse; SupportAgent::fake([ (new TextResponse('Hello', new Usage, new Meta))->withRawResponse(new Response( new Psr7Response(200, ['x-ratelimit-remaining-requests' => '99'], '{}') )),]); $response = (new SupportAgent)->prompt('Hi'); $response->raw->header('x-ratelimit-remaining-requests'); // '99'
Build the headers you want to test against, then assert that your listener did what it should. Note that the method is withRawResponse(), not withRaw().
Further Reading
$response->raw shipped in v0.10.3. The most recent release, v0.11.0, extends the same idea with lifecycle events and timings for every step and tool call in an agent run.
For background on the package, see the AI SDK's announcement and our coverage of human-in-the-loop tool approval. The source lives at laravel/ai on GitHub.