Laravel AI Agents: Streaming &amp; Structured Output | Mohamed Said        [  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png)   Mohamed Said Laravel Backend Engineer  ](https://msaied.com) [ Home ](https://msaied.com) [ Projects ](https://msaied.com/projects) [ Articles  ](https://msaied.com/articles) [ Certificates ](https://msaied.com/certificates) [ Contact ](https://msaied.com#contact-section) 

       [  ](https://github.com/EG-Mohamed)       

 [ Home ](https://msaied.com) [ Projects ](https://msaied.com/projects) [ Articles ](https://msaied.com/articles) [ Certificates ](https://msaied.com/certificates) [ Contact ](https://msaied.com#contact-section) 

  [ home ](https://msaied.com)    [ articles ](https://msaied.com/articles)    Production AI Agents in Laravel: Streaming, Token Budgets, and Structured Output Contracts        On this page       1. [  The Gap Between a Demo and a Production Agent ](#the-gap-between-a-demo-and-a-production-agent)
2. [  1. Server-Sent Events Streaming with Laravel ](#1-server-sent-events-streaming-with-laravel)
3. [  2. Enforcing Token Budgets Before the Request Leaves ](#2-enforcing-token-budgets-before-the-request-leaves)
4. [  3. Typed Structured Output Contracts ](#3-typed-structured-output-contracts)
5. [  Takeaways ](#takeaways)

  ![Production AI Agents in Laravel: Streaming, Token Budgets, and Structured Output Contracts](https://cdn.msaied.com/704/165bcb76b898fe9911130d2c93b9b805.png)

  #laravel   #ai   #llm   #php  

 Production AI Agents in Laravel: Streaming, Token Budgets, and Structured Output Contracts 
============================================================================================

     26 Sep 2026      3 min read    ![Mohamed Said](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp)  Mohamed Said  

       Table of contents

1. [  01   The Gap Between a Demo and a Production Agent  ](#the-gap-between-a-demo-and-a-production-agent)
2. [  02   1. Server-Sent Events Streaming with Laravel  ](#1-server-sent-events-streaming-with-laravel)
3. [  03   2. Enforcing Token Budgets Before the Request Leaves  ](#2-enforcing-token-budgets-before-the-request-leaves)
4. [  04   3. Typed Structured Output Contracts  ](#3-typed-structured-output-contracts)
5. [  05   Takeaways  ](#takeaways)

 The Gap Between a Demo and a Production Agent
---------------------------------------------

Most Laravel AI tutorials stop at `Http::post('https://api.openai.com/v1/chat/completions', [...])` and call it done. In production you face three hard problems: responses that take 30+ seconds must stream to the browser, runaway prompts blow your cost budget, and untyped JSON blobs from the model break your downstream logic silently. This article solves all three.

---

1. Server-Sent Events Streaming with Laravel
--------------------------------------------

OpenAI's streaming endpoint sends newline-delimited `data:` chunks. Laravel's `StreamedResponse` is the right primitive.

```php
// routes/web.php
Route::get('/agent/stream', AgentStreamController::class);

```

```php
final class AgentStreamController
{
    public function __invoke(Request $request): StreamedResponse
    {
        $messages = $request->validate(['messages' => 'required|array']);

        return response()->stream(function () use ($messages) {
            $stream = OpenAI::chat()->createStreamed([
                'model'    => 'gpt-4o',
                'messages' => $messages['messages'],
            ]);

            foreach ($stream as $response) {
                $delta = $response->choices[0]->delta->content ?? '';
                if ($delta !== '') {
                    echo 'data: ' . json_encode(['token' => $delta]) . "\n\n";
                    ob_flush();
                    flush();
                }
            }

            echo "data: [DONE]\n\n";
            ob_flush();
            flush();
        }, 200, [
            'Content-Type'      => 'text/event-stream',
            'Cache-Control'     => 'no-cache',
            'X-Accel-Buffering' => 'no', // critical for Nginx
        ]);
    }
}

```

The `X-Accel-Buffering: no` header is the most commonly forgotten detail. Without it, Nginx buffers the entire response before forwarding it.

---

2. Enforcing Token Budgets Before the Request Leaves
----------------------------------------------------

Token overruns are a billing and latency problem. Enforce a budget at the call site, not as an afterthought.

```php
final class TokenBudget
{
    public function __construct(
        private readonly int $maxPromptTokens = 3_000,
        private readonly int $maxCompletionTokens = 1_000,
    ) {}

    /** Rough estimate: 1 token ≈ 4 chars for English prose */
    public function assertPromptFits(array $messages): void
    {
        $chars = array_sum(array_map(
            fn($m) => strlen($m['content'] ?? ''),
            $messages
        ));

        $estimated = (int) ceil($chars / 4);

        if ($estimated > $this->maxPromptTokens) {
            throw new PromptTooLargeException(
                "Estimated {$estimated} tokens exceeds budget of {$this->maxPromptTokens}."
            );
        }
    }

    public function completionLimit(): int
    {
        return $this->maxCompletionTokens;
    }
}

```

Bind it as a singleton and inject it into your agent service. Pass `max_tokens` explicitly on every API call — never leave it open-ended in production.

```php
$budget->assertPromptFits($messages);

$response = OpenAI::chat()->create([
    'model'      => 'gpt-4o',
    'messages'   => $messages,
    'max_tokens' => $budget->completionLimit(),
]);

```

---

3. Typed Structured Output Contracts
------------------------------------

OpenAI's JSON mode and structured outputs return a string you must decode. Wrap that decode in a typed DTO so a schema mismatch throws immediately rather than propagating a null through your domain.

```php
final readonly class ExtractedLeadData
{
    public function __construct(
        public string $companyName,
        public string $contactEmail,
        public ?string $phoneNumber,
        public int $estimatedEmployees,
    ) {}

    public static function fromModelResponse(string $json): self
    {
        $data = json_decode($json, true, flags: JSON_THROW_ON_ERROR);

        return new self(
            companyName:         $data['company_name'] ?? throw new MalformedAgentResponseException('company_name'),
            contactEmail:        filter_var($data['contact_email'] ?? '', FILTER_VALIDATE_EMAIL)
                                     ?: throw new MalformedAgentResponseException('contact_email'),
            phoneNumber:         $data['phone_number'] ?? null,
            estimatedEmployees:  (int) ($data['estimated_employees']
                                     ?? throw new MalformedAgentResponseException('estimated_employees')),
        );
    }
}

```

Pair this with a `response_format: { type: 'json_object' }` parameter and a system prompt that describes the exact schema. The DTO constructor acts as your runtime contract — if the model drifts, you catch it at the boundary.

---

Takeaways
---------

- Set `X-Accel-Buffering: no` on every SSE response or Nginx will silently buffer your stream.
- Estimate token counts before the request and pass `max_tokens` explicitly — never leave it unbounded.
- Decode model JSON into typed DTOs at the boundary; let the constructor throw on schema violations.
- Treat `MalformedAgentResponseException` as a retryable error — models occasionally produce malformed JSON even in JSON mode.
- Keep `TokenBudget` as a named singleton so budget policy is one config change, not a grep-and-replace.

 Found this useful?

          [  ](https://twitter.com/intent/tweet?url=https%3A%2F%2Fmsaied.com%2Farticles%2Fproduction-ai-agents-in-laravel-streaming-token-budgets-and-structured-output-contracts-4&text=Production+AI+Agents+in+Laravel%3A+Streaming%2C+Token+Budgets%2C+and+Structured+Output+Contracts) [  ](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fmsaied.com%2Farticles%2Fproduction-ai-agents-in-laravel-streaming-token-budgets-and-structured-output-contracts-4) 

 Frequently Asked Questions 
----------------------------

  3 questions  

     Q01  Why does my streamed response appear all at once in the browser despite using StreamedResponse?        Almost always it is Nginx buffering the response. Add the `X-Accel-Buffering: no` response header and ensure `fastcgi_buffering off` is not overriding it at the server block level. Also confirm `ob_flush()` and `flush()` are called after each chunk. 

      Q02  Is the 1 token ≈ 4 characters estimate reliable enough for a budget guard?        It is a conservative heuristic for English text. For multilingual content or code, tokens are denser and the estimate will under-count. Use it as a pre-flight safety check, not a billing-accurate counter. For precise counts, integrate a tiktoken-compatible tokenizer library. 

      Q03  Should I retry when ExtractedLeadData::fromModelResponse throws MalformedAgentResponseException?        Yes, with a low retry limit (1–2 attempts) and a stricter system prompt on the retry. Log the raw JSON on failure so you can audit model drift over time. If failures are frequent, switch to OpenAI's structured outputs feature which enforces your JSON schema server-side. 

  Continue reading

 More Articles 
---------------

 [ View all    ](https://msaied.com/articles) 

 [ ![Decide with Jev: Build a Laravel AI Content Preflight Checker That Returns a Probability](https://cdn.msaied.com/701/916a0dd2b427c6335395d6d2684524ab.png) Laravel AI Jev TypeSafe 

### Decide with Jev: Build a Laravel AI Content Preflight Checker That Returns a Probability

Jev is a TypeSafe AI model that returns a probability score instead of text. Learn how Harris Raftopoulos uses...

  ![Mohamed Said](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp)  Mohamed Said 

 25 Sep 2026     4 min read  

  Read    

 ](https://msaied.com/articles/decide-with-jev-build-a-laravel-ai-content-preflight-checker-that-returns-a-probability) [ ![Filament v4 Migrating from v3: Breaking Changes and Refactor Patterns](https://cdn.msaied.com/700/85cddaf32751d756924869323a845563.png) filament laravel upgrade 

### Filament v4 Migrating from v3: Breaking Changes and Refactor Patterns

A practical, opinionated guide to the most impactful breaking changes when upgrading Filament v3 to v4, with c...

  ![Mohamed Said](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp)  Mohamed Said 

 25 Sep 2026     4 min read  

  Read    

 ](https://msaied.com/articles/filament-v4-migrating-from-v3-breaking-changes-and-refactor-patterns-1) [ ![Livewire v3 Islands, Lazy Components, and Deferred Loading in Practice](https://cdn.msaied.com/699/2667012bbe680cb54d99e4596e396547.png) livewire laravel performance 

### Livewire v3 Islands, Lazy Components, and Deferred Loading in Practice

Lazy components and deferred loading in Livewire v3 let you ship fast initial pages and hydrate expensive UI o...

  ![Mohamed Said](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp)  Mohamed Said 

 25 Sep 2026     1 min read  

  Read    

 ](https://msaied.com/articles/livewire-v3-islands-lazy-components-and-deferred-loading-in-practice-4) 

   [  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png)   Mohamed Said Laravel Backend Engineer  ](https://msaied.com)Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

Explore

- [Home](https://msaied.com)
- [Projects](https://msaied.com/projects)
- [Articles](https://msaied.com/articles)
- [Certificates](https://msaied.com/certificates)
- [Contact](https://msaied.com#contact-section)

Connect

- [   hello@msaied.com ](mailto:hello@msaied.com)
- [   +20 109 461 9204 ](tel:+201094619204)

© 2026 Mohamed Said. All rights reserved.

 [  ](https://github.com/EG-Mohamed) [  ](https://www.linkedin.com/in/msaiedm/) [  ](https://wa.me/201094619204) [  ](mailto:hello@msaied.com) [  ](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)
