Laravel AI SDK: Access Raw HTTP Responses and Rate Limit Headers
Laravel AI #Laravel AI #AI SDK #Rate Limiting #HTTP Client #Laravel

Laravel AI SDK: Access Raw HTTP Responses and Rate Limit Headers

3 min read Mohamed Said Mohamed Said

What Changed in Laravel AI SDK v0.10.3

Before v0.10.3, the Laravel AI SDK returned a typed response object with shared properties like $response->text, $response->usage, and $response->meta. Anything outside that common shape — rate limit headers, provider-specific request IDs, or extra JSON fields — was simply unreachable without writing your own HTTP middleware.

Version 0.10.3, released on August 6, 2026, closes that gap. A new public raw property on every response holds the Illuminate\Http\Client\Response from the underlying HTTP call:

$response = (new SupportAgent)->prompt('Summarize this document.');

$response->raw->header('x-ratelimit-remaining-requests');
$response->raw->json('id');

Because it is a standard Laravel HTTP client response, header(), json(), and status() all work exactly as they do after an Http::get() call.

Per-Step Raw Responses

An agent that calls tools makes multiple round-trips. $response->raw reflects the final request — the one that produced the text you received. Every intermediate step also keeps its own raw:

foreach ($response->steps as $step) {
    $step->raw?->header('x-ratelimit-remaining-tokens');
}

This matters for rate limit accounting: a five-step run consumed budget across five requests, and reading only the last header gives you an incomplete picture.

Monitoring Rate Limits With an Event Listener

Instead of checking headers at every call site, you can centralise the logic in an event listener. The AgentPrompted event carries the full response:

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,
        ]);
    }
});

One listener covers every agent run in your application.

Correlating Failures With the Provider

When a run produces unexpected output and you need to open a support ticket, providers ask for their own request ID. You can now log it without capturing the full prompt payload:

Log::info('Agent run completed.', [
    'invocation' => $response->invocationId,
    'provider_request_id' => $response->raw?->header('request-id'),
]);

Header names vary by provider, so check the documentation for whichever one you are using.

When raw Is Null

The property is nullable — always use the null-safe operator ?->. Four situations return null:

  • Streamed responses ($agent->stream() and AgentStreamed) — the response is assembled from stream events, not a single response body.
  • AWS Bedrock — the AWS SDK handles the HTTP call, so no Illuminate\Http\Client\Response is produced.
  • Serialized responses — Guzzle streams cannot be serialized, so raw is dropped when a response passes through a queue or cache. Read the header before dispatching a job and pass the value explicitly.
  • Faked agents — unless the fake is built with withRawResponse().

Testing Rate Limit Logic

Fake responses support withRawResponse() so you can simulate low-budget scenarios in tests:

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'

Key Takeaways

  • $response->raw is an Illuminate\Http\Client\Response available on every non-streamed, non-Bedrock response from v0.10.3 onward.
  • Each step in a multi-step agent run has its own raw, giving you per-request rate limit data.
  • The AgentPrompted event exposes raw for centralised monitoring without scattering header checks across your codebase.
  • raw is null for streamed responses, Bedrock, serialized responses, and unfaked test agents.
  • Use withRawResponse() (not withRaw()) to supply headers in fakes.

Source: Laravel AI: Get Raw HTTP Responses and Rate Limits — Laravel News

Found this useful?

Frequently Asked Questions

3 questions
Q01 Which providers populate `$response->raw` in the Laravel AI SDK?
All HTTP-based providers populate it: Anthropic, OpenAI, Azure OpenAI, DeepSeek, Gemini, Groq, Mistral, Ollama, OpenAI-compatible, OpenRouter, and xAI. AWS Bedrock does not, because the AWS SDK handles the HTTP call internally.
Q02 Why is `$response->raw` null after a queued or cached response?
The underlying Guzzle stream cannot be serialized. The SDK drops `raw` during `__serialize()` to avoid a `LogicException`. If you need a header value in a queued job, read it before dispatching and pass it as a constructor argument.
Q03 How do I test rate limit logic when `raw` is normally null in fakes?
Use `withRawResponse()` on a `TextResponse` fake, passing a `GuzzleHttp\Psr7\Response` with the headers you want to assert against. The method is `withRawResponse()`, not `withRaw()`.

Continue reading

More Articles

View all