<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Valerio</title>
    <description>The latest articles on DEV Community by Valerio (@ilvalerione).</description>
    <link>https://dev.to/ilvalerione</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F178561%2F30369dc7-f030-4cf3-8855-7af9f5143f8c.png</url>
      <title>DEV Community: Valerio</title>
      <link>https://dev.to/ilvalerione</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ilvalerione"/>
    <language>en</language>
    <item>
      <title>Debug Your PHP App From Your Phone, Through Your AI Assistant</title>
      <dc:creator>Valerio</dc:creator>
      <pubDate>Tue, 28 Jul 2026 14:53:27 +0000</pubDate>
      <link>https://dev.to/inspector/debug-your-php-app-from-your-phone-through-your-ai-assistant-3mhb</link>
      <guid>https://dev.to/inspector/debug-your-php-app-from-your-phone-through-your-ai-assistant-3mhb</guid>
      <description>&lt;p&gt;Last month I was at dinner when my phone buzzed with an Inspector alert: an error on one of our own endpoints. The old routine kicks in automatically at that point. Open the dashboard on a screen too small for it, squint at a stack trace, tell yourself "I'll look properly when I’m back at the laptop", and spend the rest of the evening with that specific low grade guilt of knowing something is broken and choosing to do nothing about it.&lt;/p&gt;

&lt;p&gt;This time I didn't do that. I opened Claude on my phone, asked it to check what was going on with that endpoint, and within a couple of messages it had pulled the actual error, traced it to a query that started timing out after a data migration earlier that day, and proposed the fix. I reviewed it, it looked right, and the incident was basically closed before the table finished eating. That gap, between getting the alert and being able to do something real about it, is what we spent the last stretch of work closing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part of the workflow nobody designed for
&lt;/h2&gt;

&lt;p&gt;We built the Inspector MCP server so your AI coding assistant could read your production data directly: errors, response times, slow queries, the same information you’d normally go dig up in the dashboard. If you missed that first release, the original announcement covers what it does.&lt;/p&gt;

&lt;p&gt;It worked well from day one, but only in one context: coding agents running on your own machine, like Claude Code or Cursor, where you paste your Inspector API token into a config file once and the tool uses it locally from then on. That's fine when the AI client lives on your laptop next to your code. It falls apart the moment the client is Claude or ChatGPT running as a hosted app in a browser or on your phone, because there's no config file there to paste a token into, and there shouldn’t be. Hosted apps authenticate through OAuth, the login-and-approve flow you already know from connecting Slack or Google Drive to some other tool, not through a secret you copy and paste. Our server simply didn’t speak that language yet, so the alert would land on your phone and the trail stopped right there. You could read that something broke. You couldn’t do anything about it without switching devices.&lt;/p&gt;

&lt;p&gt;That's the specific moment this update is built for. Not "AI assistants can now use Inspector data", which was already true, but the alert-to-fix path being unbroken end to end, on whatever device you're holding when the alert arrives.&lt;/p&gt;

&lt;h2&gt;
  
  
  Connecting Inspector to your AI account, step by step
&lt;/h2&gt;

&lt;p&gt;The whole point of this release is that there is no JSON configuration and no API token involved anymore. If you tried the first version and remember editing a config file, forget that part. Here is the actual flow, using Claude as the example, because it's the client I use most. ChatGPT and other web based agents follow the same pattern under different menu names.&lt;/p&gt;

&lt;p&gt;Step 1: add the connector with just a URL&lt;br&gt;
In Claude, open Settings and go to the Connectors section, then choose to add a custom connector. You give it a name, Inspector, and paste the server URL, which you'll find in your application settings inside the Inspector dashboard. It looks like this, with your own application ID at the end:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://app.inspector.dev/mcp?app=YOUR_APP_ID
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. You don't need to touch "Advanced settings", and there is no field asking you for an authorization token, because the server now negotiates that part on its own.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8g5c7kg9cy5osc46h2i7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8g5c7kg9cy5osc46h2i7.png" alt="Claude custom MCP server" width="800" height="608"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: authorize the connection on Inspector
&lt;/h2&gt;

&lt;p&gt;As soon as you connect, you're redirected to Inspector, where you'll see a plain authorization screen telling you which application is asking for access and what it will be able to do. In this case, Claude requesting permission to use the MCP server. You log in with the Inspector account you already have, click Authorize, and the handshake completes behind the scenes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnreb2m3rfrlptsy0zgwc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnreb2m3rfrlptsy0zgwc.png" alt="Inspector MCP server authorization" width="800" height="538"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is the piece that was missing before, and it's the reason the whole thing now works from a phone: your credentials stay between you and Inspector, and the AI client walks away with a scoped token it can refresh on its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: check the tools you just gave your assistant
&lt;/h2&gt;

&lt;p&gt;You land back in Claude with a confirmation that you're connected, and the connector page now shows the tools the Inspector MCP server exposes, along with a permission switch for each one. You'll see the ability to analyze a specific error, pull recent errors from your production environment, list recent transactions, inspect the details of a single transaction, and get the ten worst performing transactions in your application.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl8m61332u9mi62aud4z7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl8m61332u9mi62aud4z7.png" alt="Inspector MCP server tools list" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Worth noticing that these are all read only. Your assistant can look at your production data and reason about it, but it isn’t changing anything in your Inspector account. You can also decide per tool whether Claude is always allowed to use it, should ask first, or shouldn’t touch it at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: try it with one prompt
&lt;/h2&gt;

&lt;p&gt;The fastest way to confirm everything works is to ask something broad and see if it comes back with real data instead of a polite guess. Copy and paste this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Look at Inspector and let me know what kind of transactions happened in my application recently.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should get an actual summary of your traffic: the endpoints, jobs and commands that ran, how they performed, and anything that stands out. Once you see real numbers coming back, the connection is done and you can forget it exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this looks like when something actually breaks
&lt;/h2&gt;

&lt;p&gt;The setup above takes two minutes and then disappears into the background. The part that matters is the next time an alert reaches your phone while you're nowhere near a desk.&lt;/p&gt;

&lt;p&gt;You open the assistant straight from the notification and start with something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Inspector just flagged an error on the /checkout/complete endpoint. What's happening?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The assistant pulls the real error through the MCP connection: exception type, how often it's firing, when it started, which transactions are affected. Then you keep going in the same thread:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Show me the slowest transaction involved and suggest what to fix in the code.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's working from actual execution data now, not from reading your code in isolation and guessing which branch is expensive. It can propose a fix, and if your coding agent is set up with access to your repository, open a pull request or push to a branch so the change is waiting for you already reviewed by the time you sit down.&lt;/p&gt;

&lt;p&gt;This won't replace a proper debugging session for anything genuinely complex. But for the alert that hits during a commute, at dinner, or on a Sunday afternoon, the difference between "I'll deal with it tonight" and "I already dealt with it" is the whole reason we built this.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it took actual engineering work
&lt;/h2&gt;

&lt;p&gt;We could have shipped a shortcut, some long lived token you paste into a web form, and called the mobile use case supported. It would have demoed fine. It also would have meant handing a static credential to a third party service, which is the exact failure mode OAuth exists to prevent, and it would have made revoking access a manual step that's easy to forget. Doing it properly means Inspector now behaves like any other real OAuth provider: connected applications are visible and revocable from your account, tokens expire and refresh on their own, and the security model matches what you already expect when you connect one service to another.&lt;/p&gt;

&lt;p&gt;It's a small piece of infrastructure next to the monitoring itself, but it's the piece that decides whether "you can debug from your phone" is actually true or just true in a demo video. For us, it's the former now.&lt;/p&gt;

&lt;p&gt;If you haven't connected it yet, the server URL is in your application settings in the &lt;a href="https://app.inspector.dev" rel="noopener noreferrer"&gt;Inspector dashboard&lt;/a&gt;. Add it to whichever AI assistant you carry around, laptop, browser, or phone.&lt;/p&gt;

</description>
      <category>php</category>
      <category>ai</category>
      <category>webdev</category>
      <category>agents</category>
    </item>
    <item>
      <title>The Neuron Facade: Talking to Your AI Agent in Laravel</title>
      <dc:creator>Valerio</dc:creator>
      <pubDate>Wed, 15 Jul 2026 14:39:13 +0000</pubDate>
      <link>https://dev.to/inspector/the-neuron-facade-talking-to-your-ai-agent-in-laravel-3pdo</link>
      <guid>https://dev.to/inspector/the-neuron-facade-talking-to-your-ai-agent-in-laravel-3pdo</guid>
      <description>&lt;p&gt;Before this release, using Neuron AI inside Laravel meant creating a dedicated agent class, extending Agent, implementing a &lt;code&gt;provider()&lt;/code&gt; method, and wiring the system prompt yourself. That pattern is the right one once your agent has a personality, a set of tools, and a role in your application. But it is a lot of ceremony for a developer who just wants to check whether Claude, or GPT, or Gemini responds well to a given prompt, or who is prototyping a small internal feature that talks to an LLM once and does not need to be a first class citizen of the codebase.&lt;/p&gt;

&lt;p&gt;Laravel developers already know a pattern for this kind of situation: facades. A facade gives you a short, expressive entry point to a service that Laravel has already configured and bound into the container, without forcing you to resolve it manually every time. That is precisely the role the Neuron facade plays here. It reads the default AI provider and the system instructions from your &lt;code&gt;config/neuron.php&lt;/code&gt; file, so the agent is already configured by the time you touch it in your code. You are not instantiating a provider, you are not wiring credentials, you are just asking a question.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting it up
&lt;/h2&gt;

&lt;p&gt;If you have not published the configuration file yet, do it once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan vendor:publish &lt;span class="nt"&gt;--tag&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;neuron-config
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then set your provider and credentials in the environment file, the same way you would for any other Laravel service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NEURON_AI_PROVIDER=anthropic
ANTHROPIC_KEY=your-key-here
ANTHROPIC_MODEL=claude-sonnet-5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From here on, the facade is ready to use anywhere in your application code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three ways to talk to the agent
&lt;/h2&gt;

&lt;p&gt;The facade exposes the three interaction modes that cover most real world use cases, and they read the same way you would already write a controller. A synchronous chat, for when you want the full answer at once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Http\Controllers&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;NeuronAI\Laravel\Facades\Neuron&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;NeuronAI\Chat\Messages\UserMessage&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AskController&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Controller&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Neuron&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;UserMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'question'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getMessage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;response&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
            &lt;span class="s1"&gt;'answer'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getContent&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A streaming mode, for when you want to push tokens to the frontend as they arrive instead of making the user stare at a spinner:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Http\Controllers&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;NeuronAI\Laravel\Facades\Neuron&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;NeuronAI\Chat\Messages\UserMessage&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\HttpFoundation\StreamedResponse&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AskStreamController&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Controller&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;StreamedResponse&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;response&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Neuron&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;UserMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'question'&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;events&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="nb"&gt;ob_flush&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
                &lt;span class="nb"&gt;flush&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your frontend already speaks a real streaming protocol instead of raw text chunks, you don't have to write that translation layer yourself. Neuron ships adapters that turn its internal stream into the event format your frontend framework expects, and AG-UI is one of them. Pass an AGUIAdapter instance to &lt;code&gt;events()&lt;/code&gt; and the facade takes care of formatting text, tool calls, reasoning, and lifecycle events the way the protocol requires:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Http\Controllers&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;NeuronAI\Laravel\Facades\Neuron&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;NeuronAI\Chat\Messages\UserMessage&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;NeuronAI\Chat\Messages\Stream\Adapters\AGUIAdapter&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\HttpFoundation\StreamedResponse&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AgUiStreamController&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Controller&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;StreamedResponse&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$adapter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AGUIAdapter&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nv"&gt;$handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Neuron&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;UserMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'question'&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
        &lt;span class="nv"&gt;$stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$handler&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;events&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$adapter&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;response&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$stream&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$stream&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$line&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="nb"&gt;ob_flush&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
                &lt;span class="nb"&gt;flush&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$adapter&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getHeaders&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you are building on the Vercel AI SDK instead, swap in &lt;code&gt;VercelAIAdapter&lt;/code&gt; and the controller stays the same shape. Either way, your agent logic does not change, only the wire format does, which means you can point the same endpoint at different frontends without rewriting the streaming code every time.&lt;/p&gt;

&lt;p&gt;And structured output, for when you do not want prose back but a typed object you can actually work with in your application logic. This is a good fit for background jobs, where you want the LLM’s answer stored as data rather than as text:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Jobs&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Models\Lead&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;NeuronAI\Laravel\Facades\Neuron&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;NeuronAI\Chat\Messages\UserMessage&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Bus\Queueable&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Contracts\Queue\ShouldQueue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;QualifyLead&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;ShouldQueue&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Queueable&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;Lead&lt;/span&gt; &lt;span class="nv"&gt;$lead&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$profile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Neuron&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;structured&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;UserMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;lead&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="nc"&gt;LeadProfile&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;lead&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
            &lt;span class="s1"&gt;'budget'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$profile&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;budget&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'intent'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$profile&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That last example matters more than it looks. A lot of the friction people feel with LLMs in production comes from parsing free text into something a program can trust. Structured output turns the model's answer into a PHP object, which means the job can update the &lt;code&gt;Lead&lt;/code&gt; record directly instead of scraping a paragraph for the information it needs.&lt;/p&gt;

&lt;p&gt;Configuring a single call without touching the shared instance&lt;br&gt;
This is the part I paid the most attention to while designing the facade, because it is also the part most likely to bite someone in production if it is not handled carefully. &lt;code&gt;Neuron&lt;/code&gt; resolves a singleton from the container. If configuration methods mutated that singleton directly, attaching a tool or a middleware in one request could leak into the next one, since the same instance would be reused across the application lifecycle. So instead, methods like &lt;code&gt;tools()&lt;/code&gt; and &lt;code&gt;middleware()&lt;/code&gt; return a fresh, independent copy that you chain into your call, and the original shared instance stays untouched.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Http\Controllers&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;NeuronAI\Laravel\Facades\Neuron&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;NeuronAI\Chat\Messages\UserMessage&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SupportSearchController&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Controller&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Neuron&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;SearchTool&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;UserMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'question'&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getMessage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;response&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'answer'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getContent&lt;/span&gt;&lt;span class="p"&gt;()]);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Elsewhere in the app, the shared instance is untouched and still has no tools attached&lt;/span&gt;
&lt;span class="nc"&gt;Neuron&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;UserMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Hello!'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Middleware works the same way, and it targets the specific node responsible for the step you want to observe or control. If you want a human to approve every tool execution before it runs, you attach the middleware to &lt;code&gt;ToolNode&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Http\Controllers&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;NeuronAI\Laravel\Facades\Neuron&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;NeuronAI\Agent\Middleware\ToolApproval&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;NeuronAI\Agent\Nodes\ToolNode&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;NeuronAI\Chat\Messages\UserMessage&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ServerOpsController&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Controller&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Neuron&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ToolNode&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ToolApproval&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;UserMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'command'&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getMessage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;response&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'answer'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getContent&lt;/span&gt;&lt;span class="p"&gt;()]);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each interaction mode is backed by its own node, &lt;code&gt;ChatNode&lt;/code&gt; for chat(), &lt;code&gt;StreamingNode&lt;/code&gt; for stream(), &lt;code&gt;StructuredOutputNode&lt;/code&gt; for structured(), so you can be precise about which part of the pipeline your middleware is supposed to watch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Knowing when to graduate to a dedicated agent class
&lt;/h2&gt;

&lt;p&gt;The facade is not meant to replace agent classes, it is meant to sit next to them. Once your use case grows past a single call, once you need a custom memory strategy, multiple middleware working together, or a recurring identity with its own tools and instructions, the right move is to run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan neuron:agent MyAgent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and let that agent own its behavior properly. Think of the facade as the on ramp: fast to reach, honest about what it configures for you, and happy to hand things off once your idea has earned a class of its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it and tell me what is missing
&lt;/h2&gt;

&lt;p&gt;The Neuron AI Laravel SDK is open source, and the facade is one of those features that got better because people using it in real projects pushed back on the rough edges. If you build something with it, or if you hit a case it does not cover yet, the repository is the right place to bring it: &lt;a href="https://github.com/neuron-core/neuron-laravel" rel="noopener noreferrer"&gt;github.com/neuron-core/neuron-laravel&lt;/a&gt;. Open an issue if something feels off, or send a pull request if you already know how to fix it. PHP is not late to the agentic conversation, we are just having it in our own syntax.&lt;/p&gt;

</description>
      <category>php</category>
      <category>laravel</category>
      <category>ai</category>
      <category>agents</category>
    </item>
    <item>
      <title>LLM Provider Fallback in PHP: Automatic Failover in Neuron AI Router</title>
      <dc:creator>Valerio</dc:creator>
      <pubDate>Fri, 03 Jul 2026 09:21:37 +0000</pubDate>
      <link>https://dev.to/inspector/llm-provider-fallback-in-php-automatic-failover-in-neuron-ai-router-k39</link>
      <guid>https://dev.to/inspector/llm-provider-fallback-in-php-automatic-failover-in-neuron-ai-router-k39</guid>
      <description>&lt;p&gt;When I published the first article about the &lt;a href="https://inspector.dev/routing-inference-calls-between-providers-in-neuron-ai" rel="noopener noreferrer"&gt;Neuron AI Router&lt;/a&gt;, I expected questions about routing rules. Which rule to use for structured output, how to write a custom one, how the round robin behaves under load. Some of those questions arrived, but the most frequent one was different, and it wasn't really about routing at all. It was about failure. What happens to my agent when the provider goes down?&lt;/p&gt;

&lt;p&gt;It is a fair question, and if you are new to building AI applications it deserves a proper answer before we look at any code.&lt;/p&gt;

&lt;p&gt;Here is the short version. The new fallback strategy in Neuron AI Router lets you define an ordered list of LLM providers for your PHP agent. When an inference call fails with a transient error, such as a rate limit, a timeout, or an overloaded server, the same request is automatically retried on the next provider in the list. The failover is transparent: the agent never knows it happened, and the conversation continues without losing state. The rest of this article explains why this problem exists, why the usual solutions fall short, and how to configure it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why LLM providers fail in production
&lt;/h2&gt;

&lt;p&gt;An LLM provider is an external service you talk to over HTTP. Every time your agent thinks, it is making a network call to a machine you don’t control, operated by a company that is currently serving millions of other requests. These services fail in very ordinary ways. You hit a rate limit because your traffic spiked. The provider returns an “overloaded” error because their traffic spiked. A request times out. A deployment on their side causes a few minutes of elevated error rates. None of this means you did something wrong, and none of it is rare. If you keep an agent in production long enough, you will see all of these.&lt;/p&gt;

&lt;p&gt;In a classic web application, a failed call to a third party API is usually a corner of the system. You log it, maybe retry it in a queue, and the rest of the page still works. In an agent based application the inference call is not a corner, it is the heart. If the call fails, the feature is down. And agents make the situation worse in a subtle way: a single user request can trigger many inference calls, because the agent loops through tool executions, reasoning steps, and follow up messages. Every loop iteration is another chance to hit a bad moment on the provider side. The more capable your agent becomes, the more exposed it is.&lt;/p&gt;

&lt;p&gt;The instinctive fix is a try/catch with a retry. It helps with a single timeout, but it has a structural weakness: when a provider is having a bad five minutes, retrying against the same provider just means failing more slowly. What you actually want in that moment is a different provider. And this is where things used to get uncomfortable, because switching providers meant either maintaining a second configuration of your agent, or branching your application code around the failure. The failure happens deep inside the agent execution, in the middle of a tool loop, so catching it at the application level and starting over throws away everything the agent had already done.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automatic failover at the provider boundary
&lt;/h2&gt;

&lt;p&gt;The reason I'm writing about this in the context of the router package is that the router already sits at exactly the right layer to solve it. The &lt;code&gt;RouterProvider&lt;/code&gt; is a proxy that implements &lt;code&gt;AIProviderInterface&lt;/code&gt;, the same contract as every Neuron provider. It intercepts every &lt;code&gt;chat()&lt;/code&gt;, &lt;code&gt;stream()&lt;/code&gt;, and &lt;code&gt;structured()&lt;/code&gt; call at the boundary between the agent and the outside world. If a provider fails at that boundary, the router is the one component in the system that can catch the error and forward the exact same request to another provider, without the agent ever knowing that anything happened.&lt;/p&gt;

&lt;p&gt;That is what the new fallback strategy does. You define an ordered list of providers. The router sends the request to the first one. If the call fails with a transient error, the request moves to the next provider in the list, transparently. The agent receives a response as if nothing happened. The tool loop keeps running. The conversation state is untouched.&lt;/p&gt;

&lt;p&gt;The word transient matters here. Not every error deserves a retry. If your API key is invalid or your request is malformed, sending the same request to the same provider again will fail in the same way, and the router doesn't waste time pretending otherwise. Rate limits, timeouts, and server side errors are a different story: they are tied to a moment in time on a specific provider, and the same request has a good chance of succeeding somewhere else right now. The fallback logic only reacts to this second category.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuring the fallback strategy in PHP
&lt;/h2&gt;

&lt;p&gt;With this update, routing rules become optional. If all you want is resilience, the configuration is two providers and a fallback order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;NeuronAI\Agent\Agent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;NeuronAI\Router\RouterProvider&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;NeuronAI\Providers\Anthropic\Anthropic&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;NeuronAI\Providers\OpenAI\OpenAI&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;NeuronAI\Providers\AIProviderInterface&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyAgent&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Agent&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;AIProviderInterface&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;RouterProvider&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;make&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;addProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'anthropic'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Anthropic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'ANTHROPIC_API_KEY'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'claude-sonnet-4-20250514'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;addProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'openai'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'OPENAI_API_KEY'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'gpt-4o'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setFallback&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'anthropic'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'openai'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every call goes to Anthropic. If Anthropic answers with a transient error, the same call goes to OpenAI. Your agent code, your tools, your instructions, your RAG setup: nothing else changes. This is the same drop-in property the router had from day one, applied to a new problem.&lt;/p&gt;

&lt;p&gt;Fallback and routing rules also compose. The rule decides where a request should go under normal conditions, and the fallback order decides what happens when that decision meets a failing provider:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;NeuronAI\Router\Rules\MethodRule&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nc"&gt;RouterProvider&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;make&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;addProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'anthropic'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Anthropic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;...&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;addProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'openai'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;...&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;addProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'gemini'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Gemini&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;...&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setRule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;MethodRule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'anthropic'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;structured&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'openai'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setFallback&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'anthropic'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'openai'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'gemini'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here structured output goes to OpenAI and everything else to Anthropic, but if either of them is temporarily unavailable, the request finds its way to a working provider following the fallback order. The two mechanisms answer two different questions. The rule answers “who should handle this?”, the fallback answers “who can handle this right now?”.&lt;/p&gt;

&lt;p&gt;One practical note. A fallback that works silently is great for your users and dangerous for your awareness. If Anthropic has been failing for two hours and every request has been quietly served by OpenAI, you want to know, because you are probably paying different prices and getting different model behavior. Keep an eye on your monitoring when you introduce fallback, and treat a spike of fallback activations as a signal worth investigating, not just a problem the system already solved for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this can exist as a composer package
&lt;/h2&gt;

&lt;p&gt;In other ecosystems this job usually lives outside the application. You put a gateway service like OpenRouter between your code and the providers, and the gateway handles failover for you. That works, but it means adding another network hop, another vendor, another dashboard, and another place where your prompts travel.&lt;/p&gt;

&lt;p&gt;Here the whole mechanism is a composer package inside your codebase. It is versioned with your application, configured in plain PHP, and testable with your own test suite. The reason this is possible is the same one I described in the first article of this series: the Unified Messaging Layer. Because every Neuron provider knows how to translate the same message representation into its own API format, the router can take a request that just failed on Anthropic and hand it to OpenAI without any translation work in between. The fallback strategy is not a feature bolted onto the router, it is the second consequence of the same architectural property. Routing was the first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;

&lt;p&gt;Update the package with composer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require neuron-core/router
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://github.com/neuron-core/router" rel="noopener noreferrer"&gt;README on GitHub&lt;/a&gt; covers the full configuration options, including how fallback interacts with each built-in rule, and the &lt;a href="https://docs.neuron-ai.dev" rel="noopener noreferrer"&gt;Neuron AI documentation&lt;/a&gt; covers everything about providers and agents if you are starting from scratch. If you run agents in production and you have opinions about which errors should or should not trigger a fallback, open an issue or share your experience on the community channels. This feature came out of conversations with people running Neuron AI in production, and that is exactly how I’d like it to keep evolving.&lt;/p&gt;

</description>
      <category>php</category>
      <category>ai</category>
      <category>webdev</category>
      <category>llm</category>
    </item>
    <item>
      <title>Not Every Prompt Needs Your Most Expensive Model – LLM Classifier in PHP</title>
      <dc:creator>Valerio</dc:creator>
      <pubDate>Tue, 16 Jun 2026 17:58:54 +0000</pubDate>
      <link>https://dev.to/inspector/not-every-prompt-needs-your-most-expensive-model-llm-classifier-in-php-3den</link>
      <guid>https://dev.to/inspector/not-every-prompt-needs-your-most-expensive-model-llm-classifier-in-php-3den</guid>
      <description>&lt;p&gt;When I shipped the Neuron AI &lt;a href="https://inspector.dev/routing-inference-calls-between-providers-in-neuron-ai/" rel="noopener noreferrer"&gt;official router package&lt;/a&gt; a few weeks ago I received the same question from many devs, just worded differently: can it send the hard requests to the strong model and the easy ones to the cheap one? It is the most natural rule to want. It was also the one rule I could not write cleanly, and that bothered me.&lt;/p&gt;

&lt;p&gt;The router gives you a clean place to make that decision. You register a few providers, you set a rule, and the agent never knows it is talking to a proxy. But the rule has to return a provider name, and to do that judging the prompt difficulty you first need a definition of “hard” that exists in code. That is the part nobody had. The word was doing a lot of work in conversation and none of it in the editor.&lt;/p&gt;

&lt;h2&gt;
  
  
  How people fake difficulty today
&lt;/h2&gt;

&lt;p&gt;If you go looking, the workarounds all have the same shape. Some people route by prompt length, on the theory that longer means harder. In practice a one line question about Italian contract law is short and genuinely hard, while a long pasted log that you want summarised is trivial. Length measures typing, not difficulty.&lt;/p&gt;

&lt;p&gt;Others keep a list of keywords and route anything containing “legal”, “code”, or “calculate” to the premium tier. This works for a week. Then you are maintaining a dictionary forever, it misses every phrasing you did not anticipate, and it has no opinion at all about prompts in a language you did not hard code.&lt;/p&gt;

&lt;p&gt;The most honest attempt is to ask an LLM to rate the difficulty of the prompt before you answer it. It even works reasonably well. The problem is that you are now paying for a model call, and waiting for it, in order to decide whether to make a model call. You have added latency and cost to the exact path you were trying to make cheaper. For something that runs on every single request, that is the wrong trade.&lt;/p&gt;

&lt;h2&gt;
  
  
  A score that comes from your own models
&lt;/h2&gt;

&lt;p&gt;The new package, &lt;code&gt;neuron-core/llm-classifier&lt;/code&gt;, takes a different position. It builds a small classifier that reads an incoming prompt and returns a difficulty score between 0 and 1, where 0 means your models find this easy and 1 means they struggle. The important word there is your. The score is not a generic guess about what is hard in the abstract. It is learned from the models you actually route between, so it reflects what your lineup finds hard, which is the only thing that matters when you are deciding which of your models should answer.&lt;/p&gt;

&lt;p&gt;It runs in pure PHP. The only requirement is &lt;code&gt;ext-mbstring&lt;/code&gt;. There is no Python sidecar to deploy, no GPU, no inference server sitting next to your app waiting to be restarted at three in the morning. Training happens once, offline. Scoring runs in microseconds, in process, before you ever open a socket to a provider. On every request you get a number, and the number costs you nothing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require neuron-core/llm-classifier
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Two phases, kept strictly apart
&lt;/h2&gt;

&lt;p&gt;The mental model is two activities that happen at very different times, and the package keeps them properly separated.&lt;/p&gt;

&lt;p&gt;The first is calibration. This is where you teach the classifier what easy and hard look like for your tasks and your models, and it happens once, offline, from a script or a console command. The output is a single &lt;code&gt;model.bin&lt;/code&gt; file that you commit alongside your code. When your models improve or your prices change, you re-run calibration with the new lineup and replace the file. Nothing about this lives on the request path.&lt;/p&gt;

&lt;p&gt;The second is scoring, and that is the only part that runs in your live application. You load &lt;code&gt;model.bin&lt;/code&gt; once, ideally on boot or inside your Octane, RoadRunner, or FrankenPHP workers, and then you call it on each request to get the score. Train time and run time never touch each other, which is exactly the property you want when something runs in front of every inference call.&lt;/p&gt;

&lt;p&gt;If you are wondering how a few hundred example prompts turn into a number, the short version is that words become numbers first. The package uses a free, downloadable word vector dictionary from fastText, which maps every word to a list of 300 numbers that capture its meaning, so that “buy” and “purchase” land close together while “king” and “carburetor” land far apart. Each prompt is reduced to one averaged fingerprint of those numbers, and that fingerprint is the classifier’s only input. You do not touch any of this math directly. You provide prompts, answers, and a way to grade them, and the classifier works out which patterns are hard. The pieces of the dictionary your data actually uses get baked into &lt;code&gt;model.bin&lt;/code&gt;, so the original fastText file is not needed at runtime.&lt;/p&gt;

&lt;p&gt;Training your first classifier in a couple of minutes&lt;br&gt;
You do not have to assemble your own dataset to see this working, and I would not recommend starting there. The package ships with a ready to use dataset derived from the public RouterBench benchmark, a stratified sample of around 1,845 prompts that already carries a precomputed difficulty label for each one. Because the difficulty is already known, this path needs no model panel, no graders, and no API calls at all. You only need the fastText vectors and a few seconds of CPU.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1) one-time: download the fastText vectors&lt;/span&gt;
curl &lt;span class="nt"&gt;-O&lt;/span&gt; https://dl.fbaipublicfiles.com/fasttext/vectors-crawl/cc.en.300.vec.gz
&lt;span class="nb"&gt;gunzip &lt;/span&gt;cc.en.300.vec.gz
&lt;span class="nb"&gt;mv &lt;/span&gt;cc.en.300.vec storage/

&lt;span class="c"&gt;# 2) run calibration, which writes storage/model.bin&lt;/span&gt;
php script/routerbench.php
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is your first model trained. Loading it and scoring a prompt is two lines.&lt;/p&gt;

&lt;p&gt;RouterBench records, for ~36k prompts, whether each of 11 of the most used LLMs answered correctly (models from OpenAI, Anthropic, Mistral, and other common providers). If you use a subset of this models it's already a reliable dataset. We turn that into a ready to use dataset to train your classifier.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;NeuronCore\Classifier\Classifier&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nv"&gt;$scorer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Classifier&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'storage/model.bin'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$score&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$scorer&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;overall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$userPrompt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 0 = easy, 1 = hard&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;overall()&lt;/code&gt; gives you one number to threshold against. Under the hood it is the maximum across the per-capability scores, not the average, and that choice is deliberate. A prompt that is hard at one thing and trivial at five others should be treated as hard, and an average would quietly water that down.&lt;/p&gt;

&lt;h2&gt;
  
  
  Plugging it into the router
&lt;/h2&gt;

&lt;p&gt;This is the part I had been waiting to write. The score on its own is just a number. It becomes useful the moment the router can act on it, and the wiring is small. Here is the explicit version using the router’s &lt;code&gt;CallbackRule&lt;/code&gt;, which makes every step visible.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;NeuronAI\Router\Rules\DifficultyRule&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;NeuronCore\Classifier\Classifier&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;


&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyAgent&lt;/span&gt; &lt;span class="n"&gt;extens&lt;/span&gt; &lt;span class="nc"&gt;Agent&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;AIProviderInterface&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Load the classifier ONCE (e.g. on app boot or under a long-lived worker).&lt;/span&gt;
        &lt;span class="nv"&gt;$scorer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Classifier&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'storage/model.bin'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;RouterProvider&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;make&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;addProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'mini'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'OPENAI_API_KEY'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'gpt-4o-mini'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;addProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'4o'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'OPENAI_API_KEY'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'gpt-4o'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;addProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'o1'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'OPENAI_API_KEY'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'o1'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setRule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DifficultyRule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$scorer&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;outOfDomain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'o1'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;coverage&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// unfamiliar prompt → most capable&lt;/span&gt;
                    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;easy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'mini'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;maxScore&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.33&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="c1"&gt;// overall() &amp;lt; 0.33 → cheap &amp;amp; fast&lt;/span&gt;
                    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;medium&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'4o'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;maxScore&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.70&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="c1"&gt;// overall() &amp;lt; 0.70 → solid all-rounder&lt;/span&gt;
                    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;hard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'o1'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                       &lt;span class="c1"&gt;// otherwise → most capable&lt;/span&gt;
            &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The router now ships a &lt;code&gt;DifficultyRule&lt;/code&gt; that wraps exactly this pattern. You give it the loaded classifier and your providers, it performs the coverage guard and the threshold routing for you, and the whole block above collapses into a single rule on the router.&lt;/p&gt;

&lt;p&gt;As far as I know this is the first time a prompt difficulty classifier has been wired into a production framework in pure PHP, and it’s the part I am quietly pleased about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two knobs, and how to turn them
&lt;/h2&gt;

&lt;p&gt;There are only two things to tune, and you tune them with data rather than intuition. The difficulty cut-offs, the 0.33 and 0.70 above, decide where easy ends and hard begins. The coverage cut-off, the 0.4, decides how unfamiliar a prompt has to be before you stop trusting the score. The way to set them is to log three things for real traffic: the difficulty score, the coverage, and the provider you would have chosen, then adjust until you are happy with the balance. If cheap-model answers start coming back wrong, lower the hard threshold so more requests climb to a stronger model. If out of domain prompts are leaking through to the cheap tier, raise the coverage cut-off. You are not guessing. You are reading your own logs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this leaves us
&lt;/h2&gt;

&lt;p&gt;For a long time the practical answer to “which model should answer this?” in PHP was either a static choice, or a pile of string matching you maintained by hand. Now there is a measured answer that costs microseconds and comes from your own models, and it drops into a router that was already part of the framework. The same quality where it matters, a smaller bill everywhere else, and no delay at runtime.&lt;/p&gt;

&lt;p&gt;The package is &lt;code&gt;neuron-core/llm-classifier&lt;/code&gt;, it is MIT licensed, and the RouterBench dataset is in the box so you can have a working model before you finish your coffee.&lt;/p&gt;

&lt;p&gt;Train your first LLM classifier now: &lt;a href="https://github.com/neuron-core/llm-classifier" rel="noopener noreferrer"&gt;https://github.com/neuron-core/llm-classifier&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>php</category>
      <category>agents</category>
    </item>
    <item>
      <title>Mixing LLM Providers Inside a Neuron AI Agent</title>
      <dc:creator>Valerio</dc:creator>
      <pubDate>Wed, 03 Jun 2026 07:53:04 +0000</pubDate>
      <link>https://dev.to/inspector/mixing-llm-providers-inside-a-neuron-ai-agent-70g</link>
      <guid>https://dev.to/inspector/mixing-llm-providers-inside-a-neuron-ai-agent-70g</guid>
      <description>&lt;p&gt;When I started the v3 of Neuron AI, the first big decision I had to make was not about agents or tools, but about messages. Each LLM provider has its own way of describing a conversation: OpenAI uses one shape, Anthropic another, Gemini and Ollama add their own variations on top. I could have written thin wrappers and let each provider speak its native dialect, pushing the complexity back to the application developer. Instead, I spent a lot of time on what I now call the Unified Messaging Layer: a single representation of messages, content blocks, and tools, that every provider knows how to translate into its own format.&lt;/p&gt;

&lt;p&gt;That work felt almost invisible from the outside. People want to see agents, RAG, workflows, the visible parts of a framework. A messaging layer is plumbing, and plumbing is boring until the day it lets you do something you didn't plan for. Last week, while sketching out a few changes requested by developers running production agents, I realized that this old design choice had quietly enabled a feature I hadn’t explicitly designed: routing a single inference call to different providers, transparently to the agent itself.&lt;/p&gt;

&lt;p&gt;That’s what the new &lt;a href="https://github.com/neuron-core/router" rel="noopener noreferrer"&gt;neuron-core/router&lt;/a&gt; package is. It exposes a &lt;code&gt;RouterProvider&lt;/code&gt; that implements AIProviderInterface, the same contract every Neuron provider implements. From the agent’s perspective, it is just another provider. Under the hood, every call to &lt;code&gt;chat()&lt;/code&gt;, &lt;code&gt;stream()&lt;/code&gt;, or &lt;code&gt;structured()&lt;/code&gt; is delegated to one of several registered providers, chosen by a routing rule you control.&lt;/p&gt;

&lt;p&gt;It's like OpenRouter but inside your code :). Here is the smallest example I can write:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;NeuronAI\Agent\Agent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;NeuronAI\Router\RouterProvider&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;NeuronAI\Router\Rules\RoundRobinRule&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;NeuronAI\Providers\Anthropic\Anthropic&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;NeuronAI\Providers\OpenAI\OpenAI&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;NeuronAI\Providers\AIProviderInterface&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyAgent&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Agent&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;AIProviderInterface&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;RouterProvider&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;make&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;addProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'anthropic'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Anthropic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'ANTHROPIC_API_KEY'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'claude-sonnet-4-20250514'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;addProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'openai'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'OPENAI_API_KEY'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'gpt-4o'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setRule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RoundRobinRule&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'anthropic'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'openai'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
            &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent class is unchanged in every other way. Instructions, tools, observers, RAG: they all keep working exactly as before. The router is a drop-in replacement for a single provider.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters in practice
&lt;/h2&gt;

&lt;p&gt;I want to walk through the scenarios that came up most often in conversations with people building real things on top of Neuron AI, because the value of the router only becomes clear when you see it next to a concrete problem.&lt;/p&gt;

&lt;p&gt;The first one is structured output. Some models follow JSON schemas more reliably than others, especially when the schema gets nested or strict. A team I was talking to a few weeks ago had standardized on Claude for the conversational quality of their agent, but kept hitting edge cases when extracting structured data. The honest answer is: use a different model for that specific call. Until now, that meant maintaining two agents or branching the code at the call site. With the router, you can just pass a rule:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$router&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setRule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;MethodRule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'anthropic'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;structured&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'openai'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every &lt;code&gt;chat()&lt;/code&gt; goes to Claude, every &lt;code&gt;structured()&lt;/code&gt; goes to GPT, and the agent code stays the same.&lt;/p&gt;

&lt;p&gt;The second scenario is multimodality. Providers don't all support the same content blocks, and even when they do, the quality and pricing vary. Gemini handles video natively, OpenAI does well with some file formats, others are at their best on plain reasoning. The &lt;code&gt;ContentRule&lt;/code&gt; inspects the message content and routes accordingly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$router&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setRule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ContentRule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'anthropic'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;image&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'gemini'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;video&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'gemini'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'openai'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When a user attaches a video to the agent, the request silently lands on Gemini. When the same user sends a plain text question right after, it goes back to Anthropic. The agent has no idea this is happening, and neither does the application code that drives it.&lt;/p&gt;

&lt;p&gt;The third scenario is cost and resilience. Round-robin distribution between two providers can spread load across rate limits and reduce the blast radius of a temporary outage on one side. When the logic gets more nuanced, the &lt;code&gt;CallbackRule&lt;/code&gt; lets you write any custom decision you want, with access to the method being called, the messages, and the tools attached to the request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$router&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setRule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CallbackRule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$tools&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$tools&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="s1"&gt;'anthropic'&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'openai'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For anything beyond what callbacks comfortably express, you can implement RoutingRuleInterface directly and use any signal you want, including request metadata, token estimates, time of day, or whatever your application exposes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this works at all
&lt;/h2&gt;

&lt;p&gt;I mentioned earlier that the &lt;a href="https://docs.neuron-ai.dev/agent/messages" rel="noopener noreferrer"&gt;Unified Messaging Layer&lt;/a&gt; is what made this package possible. It is worth being concrete about why.&lt;/p&gt;

&lt;p&gt;Routing between providers only works if the same message can be sent to any of them without the agent code adapting to each one. In Neuron AI, a &lt;br&gt;
&lt;code&gt;UserMessage&lt;/code&gt; carries a list of &lt;code&gt;ContentBlock&lt;/code&gt; objects: text, image, file, audio, video, reasoning. Each provider implementation knows how to map these blocks into its own request and response format. The router doesn’t need to know anything about the messages it forwards, because the providers themselves take care of the translation step at the boundary.&lt;/p&gt;

&lt;p&gt;The fact that I can ship a small proxy that works with every existing Neuron provider, including future ones, is a direct consequence of a decision made when a few months ago approaching v3.&lt;/p&gt;

&lt;p&gt;That's the part I find more interesting than the package itself. Small architectural choices made early on tend to compound. The Unified Messaging Layer was not designed with routing in mind, it was designed to keep agent code independent of the LLM behind it. But the same property that makes the agent independent of one provider also makes it indifferent to which provider answers each call. The router is the natural extension of that property.&lt;/p&gt;
&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;

&lt;p&gt;Install the package with composer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require neuron-ai/router
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://github.com/neuron-core/router" rel="noopener noreferrer"&gt;README&lt;/a&gt; on GitHub covers the built-in rules in detail, including the use of a default provider for the cases where the agent needs access to the underlying message and tool mappers before any inference call has been made. If you build something interesting with custom rules, in particular logic involving cost estimation or fallback behaviors, please share it on the community channels. There is a clear space for community-contributed rules, and &lt;code&gt;neuron-ai/router&lt;/code&gt; is built so that adding one is just an implementation of a single interface.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/neuron-core/router" rel="noopener noreferrer"&gt;https://github.com/neuron-core/router&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>php</category>
      <category>agents</category>
    </item>
    <item>
      <title>Neuron AI Started From Fear - The True Story</title>
      <dc:creator>Valerio</dc:creator>
      <pubDate>Mon, 25 May 2026 10:15:55 +0000</pubDate>
      <link>https://dev.to/inspector/neuron-ai-started-from-fear-the-true-story-2jcl</link>
      <guid>https://dev.to/inspector/neuron-ai-started-from-fear-the-true-story-2jcl</guid>
      <description>&lt;p&gt;In late 2024 the parts of the internet I follow filled up with posts about AI agents. YouTube tutorials. Reddit threads. Blog after blog. Conference recordings. And underneath all of it, one technical stack: Python. LangChain. LangGraph. The vocabulary of an entire field was being written in a language I had never used.&lt;/p&gt;

&lt;p&gt;In my corner of the internet, the PHP corner, there was silence. No threads about agents. No serious discussion about how one of the top programming languages in the world, the king of web's backends was going to participate in what looked like the largest shift in application development in a decade. As CTO of a product company whose entire stack is PHP, that silence scared me. Inspector exists because thousands of teams ship PHP into production every day and need to know what's happening inside those applications. If those applications were about to gain a new layer that none of us could build, then the company I had co-founded with Sebastiano and Francesco was sitting on top of a foundation the industry was quietly walking away from.&lt;/p&gt;

&lt;p&gt;Fear is what moved me at the beginning. Not strategy. Not ambition. Just the very specific dread of watching your professional world become invisible while the rest of the industry holds a conversation you weren't invited to.&lt;/p&gt;

&lt;p&gt;So I started learning. I read about agents, tools, memory, RAG, workflows. I built small things in Python because there was no other option. And somewhere in that period, line of code after line of code, the fear started turning into something else. A question. If I was scared, and I had a company behind me, what about the freelancer in Naples or Krakow or Lagos who was watching their pipeline slowly tilt away from PHP work toward “AI integration” they had no way to deliver? What about the senior engineer at a SaaS company being asked, in the next planning meeting, whether the team could “do something with agents,” and quietly realizing the honest answer was no? What about the team lead whose company had spent fifteen years building a custom PHP platform and was suddenly wondering whether all of it had hit a dead end?&lt;/p&gt;

&lt;p&gt;In Python the agent movement was a system. Books, courses, frameworks, model providers integrating natively, conferences. In PHP it was just an unknown. And unknowns, in software, do not stay unknown for long. They either get filled by someone who shows up to do the work, or the void itself becomes the answer, and the answer is “you're irrelevant now.”&lt;/p&gt;

&lt;p&gt;I decided to fill the unknown.&lt;/p&gt;

&lt;h2&gt;
  
  
  The First Version
&lt;/h2&gt;

&lt;p&gt;The first version of Neuron didn't try to be a complete framework. It didn't even remotely resemble the ecosystem you see today. It tried to be honest. PHP developers don't need a translation of LangChain. They need PHP-native abstractions for agents, tools, RAG, structured output, and the dozen other primitives that make agentic applications feel like applications and not glued-together demos. The first goal was just to make it possible to experiment without leaving the language.&lt;/p&gt;

&lt;p&gt;I shipped v1 without much fanfare. I didn't know what to expect.&lt;/p&gt;

&lt;p&gt;What happened was the first signal that this story wasn't going to be mine alone.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.linkedin.com/in/trela-pawel/" rel="noopener noreferrer"&gt;Pawel Trela&lt;/a&gt;, a developer from Poland I had never met, found Neuron in those early weeks and used it to build Devtimate, an AI estimation tool for software projects, the kind of product that needs the agent layer to actually function. He built it on Neuron v1, when v1 was barely a framework and more of a working hypothesis. A few months later, the company exited. I remember reading that news and sitting still for a moment. I had been looking for permission to keep going, and Pawel had given it to me without knowing he was. Someone had built a real business on top of a thing I had shipped from fear.&lt;/p&gt;

&lt;p&gt;Pawel did not disappear after the exit. He stayed in the community. When the project needed a new website, he designed and built one and donated it. The current &lt;a href="https://neuron-ai.dev/" rel="noopener noreferrer"&gt;neuron-ai.dev&lt;/a&gt; is his work. I'm grateful in a way that is hard to put into a sentence. He didn't owe us anything. He showed up because he wanted to.&lt;/p&gt;

&lt;p&gt;The next one was Alessandro Astarita, CTO of &lt;a href="https://capri.com/" rel="noopener noreferrer"&gt;Capri.com&lt;/a&gt;, one of the most important booking portals in Italy, found the early Neuron and used it to rebuild parts of his platform. Alessandro didn't just use Neuron. He reshaped the RAG module from the inside, contributed back, and is now building the next chapter of Capri.com on top of the framework. When we run our first live event the next week, "&lt;em&gt;AI Agents in Action&lt;/em&gt;", Alessandro will be one of the speakers. The heroes return home and gather, but some of them helped build the house.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Messages I Wasn't Expecting
&lt;/h2&gt;

&lt;p&gt;The thing I did not anticipate was the inbound. The emails. The GitHub discussions.&lt;/p&gt;

&lt;p&gt;In December 2025 a developer named Mark opened a discussion on the Neuron repository. He started with appreciation, and then wrote something that I read three times the first time I saw it that the project was giving him hope his company's "15 year investment in a custom PHP platform has not hit a dead end". Fifteen years. The rest of his message was a critique: he wanted documentation that didn't presume Laravel, examples that would work without it, a faster path to RAG. I took the critique gladly because the critique was the easy part. The first line was the part I had been working for without knowing. That message lives publicly on discussion &lt;a href="https://github.com/neuron-core/neuron-ai/discussions/420" rel="noopener noreferrer"&gt;#420&lt;/a&gt;, and I think about it often.&lt;/p&gt;

&lt;p&gt;There were others. &lt;a href="https://github.com/neuron-core/neuron-ai/discussions/156#discussioncomment-13436693" rel="noopener noreferrer"&gt;A developer wrote&lt;/a&gt; about being on the verge of being moved off his team because his company was shifting "the AI part" toward a Python contractor. A freelancer wrote about losing two retainers in the same quarter to agencies that promised "agent integration" without specifying how. Junior developers asked whether they should give up on PHP entirely and relearn the basics in another language to stay employable.&lt;/p&gt;

&lt;p&gt;I don't have an answer for all of them. I have one answer: you can build agentic systems in PHP now. The unknown is filled. You don’t have to leave.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Pattern
&lt;/h2&gt;

&lt;p&gt;In open ecosystems I have noticed a recurring pattern on several occasions. Larger ecosystems notice things late. When they finally do notice, the size of their reach lets them write the story as if they were the protagonists from the beginning. This is not malice. It is how attention works. The audience that hears about something first tends to assume that’s where it started.&lt;/p&gt;

&lt;p&gt;What that pattern tries to do, is ignore the years of professional work that came before. What it actually does is something else.&lt;/p&gt;

&lt;p&gt;A blog post can present its ecosystem as the first to do something. It cannot change the developers running Neuron in production. It cannot change the contributors whose names fill the changelog. It cannot change Pawel in Poland, who built a product and then went on to donate the new Neuron website. It cannot change Alessandro in Naples, who rebuilt the RAG module from the inside and is now running the next chapter of Capri.com on the framework. It cannot change Mark, whose company can keep building on fifteen years of PHP investment because Neuron exists. The git log knows what was built when. The community knows. The production deployments know.&lt;/p&gt;

&lt;p&gt;What is actually at risk in attempts to dismiss the work of professinals contributors is the credibility of the people making them. Reputation in open source is one of the few currencies that cannot be inflated. The PHP ecosystem is large enough for many efforts. LLPhant, Prism, and others have done real work, and the developers building AI agents know exactly who shipped what, and when.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Cost
&lt;/h2&gt;

&lt;p&gt;I want to be honest about the part that doesn't show up in blog posts.&lt;/p&gt;

&lt;p&gt;The decision to commit Inspector's resources to Neuron was not a quick one. Inspector is a product company with paying customers, a roadmap, and a team. Spending company time on what was, at the start, an open-source framework with no obvious commercial path was not a no-brainer. It was a conversation. Sebastiano, our CEO, and Francesco, who runs the marketing side, both had to look at the same question I was looking at and decide whether the bet was worth making.&lt;/p&gt;

&lt;p&gt;We made the bet for two reasons. The first is that Inspector is natively integrated into Neuron, which means anyone building production agents in PHP has a monitoring layer available the moment they need one. The second is harder to put into a slide. We believed, and still believe, that the PHP developer community deserved a serious answer to the agent question, and we were in a position to provide one. That kind of belief is not a strategy. It is a friendship and a shared desire to do something that matters.&lt;/p&gt;

&lt;p&gt;I would not have shipped Neuron alone. I shipped it with them and thousands of PHP developers coming from every corner of the world.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where We Are Now
&lt;/h2&gt;

&lt;p&gt;Neuron is at v3 now. The framework includes agents, tools, toolkits, RAG and data loaders, async and parallel execution, workflows, structured outputs, Evaluation, multimodality. &lt;a href="https://github.com/neuron-core/maestro" rel="noopener noreferrer"&gt;Maestro&lt;/a&gt; is a general-purpose CLI agent runtime built on Neuron. &lt;a href="https://github.com/neuron-core/ai-form" rel="noopener noreferrer"&gt;AIForm&lt;/a&gt; gives PHP developers a way to handle data collection through agents. &lt;a href="https://github.com/neuron-core/router" rel="noopener noreferrer"&gt;Router&lt;/a&gt; to forward inference calls to different underlying providers based on a routing strategy you define.&lt;/p&gt;

&lt;p&gt;AI providers and companies are reaching out instead of being chased.&lt;/p&gt;

&lt;p&gt;After working so hard together, the developers are now creating a whole new class of products, looking to the future with confidence, not fear. At the beginning, none of this was the case. At the beginning it was just adventure.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI Agents in Action
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2xzcmqkluognc53usqif.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2xzcmqkluognc53usqif.jpg" alt="AI Agents In Action PHP" width="800" height="255"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In a few weeks we are running our first live event in Italy, "&lt;a href="https://www.tickettailor.com/events/inspectorsrl/2111250" rel="noopener noreferrer"&gt;AI Agents in Action&lt;/a&gt;". It's a local event, most of the people reading this blog won't be able to attend, but it matters to me because it is the first time the people who built this thing and the people who use it will be in the same room. Alessandro will be there. So will others whose names you will learn that day.&lt;/p&gt;

&lt;p&gt;The hero's journey, when it ends, is supposed to bring the hero home. The agent revolution sent a lot of PHP developers into Python territory for a while, where they didn't recognize the vocabulary and weren't sure they belonged. The point of the event, and the point of this article, is to say that there is a way home, and that other people made it back too.&lt;/p&gt;

&lt;p&gt;If you've read this far and you're a PHP developer who has been quietly wondering whether to leave the language, I have a small ask. Don't leave yet. Try this first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Start building with Neuron AI — The First Agentic Framework Of The PHP Ecosystem: &lt;a href="https://neuron-ai.dev" rel="noopener noreferrer"&gt;https://neuron-ai.dev&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>php</category>
      <category>webdev</category>
      <category>ai</category>
      <category>agents</category>
    </item>
    <item>
      <title>Your AI Agent Has Too Many Tools — Here's the Fix</title>
      <dc:creator>Valerio</dc:creator>
      <pubDate>Thu, 21 May 2026 14:18:49 +0000</pubDate>
      <link>https://dev.to/inspector/your-ai-agent-has-too-many-tools-heres-the-fix-41ig</link>
      <guid>https://dev.to/inspector/your-ai-agent-has-too-many-tools-heres-the-fix-41ig</guid>
      <description>&lt;p&gt;When I started building agents in PHP, the tool list felt like a feature to celebrate. Connect an email toolkit, a calendar, a CRM, a couple of MCP servers, and suddenly your agent can do almost anything. The problem is that "almost anything" comes with a cost that doesn't show up until you put the agent in front of real users, and by then it's already shaping your token bill and your reliability metrics in ways that are hard to trace.&lt;/p&gt;

&lt;p&gt;The mechanics are simple enough to describe. Every time the provider is invoked, Neuron AI loads every registered tool and transmits the entire catalog to the backend LLM. Each tool carries its name, description, parameter schema, and usage hints. A handful of tools is harmless. A production agent wired into a real business stack can easily reach hundreds, and at that point you're paying for the full catalog on every turn, regardless of whether the agent actually needs any of those capabilities for the current request.&lt;/p&gt;

&lt;p&gt;The token cost is the obvious problem, and it's the one most developers notice first. The less obvious problem is the one that actually breaks agents in production. When a model sees too many tools at once, descriptions blur together, similar-sounding tools start competing for attention, and the agent begins making subtly wrong choices. It mixes up parameters between tools that look alike. It hallucinates arguments because it's trying to hold too many signatures in working memory at the same time. The failure mode isn't a clean error you can catch in a try block — it's an agent that quietly degrades as your toolkit grows, which is exactly the opposite of what you want as you scale.&lt;/p&gt;

&lt;p&gt;ToolSearch is the new middleware I introduced in NeuronAI to address this. The idea is to stop treating the tool catalog as something the agent carries on every request, and start treating it as something the agent queries on demand. You wrap your full set of tools — your custom tools, your toolkits, the tools exposed by your MCP connectors — inside the ToolSearchMiddleware, and you keep your global middleware list minimal. The agent starts each turn with a small, deliberate tool set: usually just ToolSearch itself, plus whatever core tools you always want available, like a web search or a calculator.&lt;/p&gt;

&lt;p&gt;When the agent needs a capability it doesn't currently have, it calls ToolSearch with a natural language query. The middleware returns a ranked list of tool descriptors with their full schemas, inspects the result, looks the tools up in the internal registry, and injects their full definitions into the tools array for the next inference call. From the model's perspective, the next turn simply arrives with a richer tool list, and it can invoke any of those newly surfaced tools directly with proper schema validation, exactly as if they had been there from the start.&lt;/p&gt;

&lt;p&gt;You havo no limits on the number of tools you can make available to the agent through search.&lt;/p&gt;

&lt;p&gt;In the video below I walk through the middleware setup, show how ToolSearch interacts with the global middleware system and the core tool list, and explain the flow that takes a search query and turns it into a fully schema-validated tool call on the following turn. If you're building anything beyond a toy agent in PHP — anything connected to real business systems, real MCP servers, real customer data — this is the piece I'd encourage you to watch before your tool count gets ahead of you.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/qYmidHAXEYM"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Neuron AI Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Documentation for ToolSearch: &lt;a href="https://docs.neuron-ai.dev/agent/middleware#tool-search" rel="noopener noreferrer"&gt;https://docs.neuron-ai.dev/agent/middleware#tool-search&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Neuron AI Repository: &lt;a href="https://github.com/neuron-core/neuron-ai" rel="noopener noreferrer"&gt;https://github.com/neuron-core/neuron-ai&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Inspector (Observability Tool): &lt;a href="https://inspector.dev" rel="noopener noreferrer"&gt;https://inspector.dev&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Drop a comment under the video or open a discussion on the repo. And if the project is useful to you, share and star the GitHub repository is the most effective things you can do to help other PHP developers find it.&lt;/p&gt;

</description>
      <category>php</category>
      <category>ai</category>
      <category>webdev</category>
      <category>agents</category>
    </item>
    <item>
      <title>Mastering Agentic Workflows in PHP: Behind The Scenes Of Developer Contributions</title>
      <dc:creator>Valerio</dc:creator>
      <pubDate>Tue, 12 May 2026 10:15:26 +0000</pubDate>
      <link>https://dev.to/inspector/mastering-agentic-workflows-in-php-parameter-aware-tool-tracking-neuron-ai-566-ckk</link>
      <guid>https://dev.to/inspector/mastering-agentic-workflows-in-php-parameter-aware-tool-tracking-neuron-ai-566-ckk</guid>
      <description>&lt;p&gt;The first time I saw the notification for a new issue in the &lt;a href="https://github.com/neuron-core/neuron-ai" rel="noopener noreferrer"&gt;Neuron AI repository,&lt;/a&gt; I felt that familiar mix of excitement and mild anxiety that every maintainer knows well. Growing an open source project from a personal set of scripts into a framework used by others changes how you look at a code editor. You start to realize that while you might be the one merging the code, the roadmap is actually being written by the people who are struggling with real-world implementations in their own production environments. It is a shift from solving your own problems to understanding the friction points of a thousand different developers at once.&lt;/p&gt;

&lt;p&gt;When you are working as a single developer on a specific task, your mental model is usually focused on the immediate "how" of the implementation. You want the agent to call the function, get the data, and move on. However, the maintainer mindset requires a broader perspective that considers the "what if" scenarios across the entire ecosystem.&lt;/p&gt;

&lt;p&gt;In the video below I give you an example of how the two mental models collaborate helping the framework becoming better and better.&lt;/p&gt;

&lt;p&gt;This specific update for parameter-aware tool tracking was born exactly from this intersection. Users were reporting that their agents failing to recognize when a tool had been called with different input arguments. A developer sees this as a limit in their specific implementation, but a maintainer sees it as an opportunity to build a systemic change that benefits everyone.&lt;/p&gt;

&lt;p&gt;Here ir the Pull Request: &lt;a href="https://github.com/neuron-core/neuron-ai/pull/566" rel="noopener noreferrer"&gt;https://github.com/neuron-core/neuron-ai/pull/566&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The beauty of this contamination between the user's immediate needs and the maintainer's structural goals is that it produces more resilient software. By introducing the ability to track tool runs based on a unique key derived from the tool name and its specific parameters, the contributor solved a structural limit for his use case. If an agent calls a weather tool for New York and then for London, those are distinct actions that should both proceed. If it tries to call New York five times in a row with the same parameters, the framework can now intervene because it recognizes the redundancy.&lt;/p&gt;

&lt;p&gt;Even if the goal was clear, and the implementation good, there was room for generalizing this strategy. Learn more on the video below.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/LhoOQD2Jlc8"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

</description>
      <category>php</category>
      <category>ai</category>
      <category>webdev</category>
      <category>agents</category>
    </item>
    <item>
      <title>Parallel Branches in Neuron AI Workflow</title>
      <dc:creator>Valerio</dc:creator>
      <pubDate>Tue, 05 May 2026 09:39:50 +0000</pubDate>
      <link>https://dev.to/inspector/parallel-branches-in-neuron-ai-workflow-24lc</link>
      <guid>https://dev.to/inspector/parallel-branches-in-neuron-ai-workflow-24lc</guid>
      <description>&lt;p&gt;One of the things I didn't expect when I started building Neuron AI was how much the design of the framework would be shaped by the people using it. I started this project to solve my own problems: I wanted PHP developers to have a clean, idiomatic way to integrate AI into their applications without having to learn Python or rewire their entire mental model. But at some point, the users started driving the direction more than I did. That's probably the clearest signal that something is actually being used in the real world.&lt;/p&gt;

&lt;p&gt;Issue &lt;a href="https://github.com/neuron-core/neuron-ai/issues/530" rel="noopener noreferrer"&gt;#530&lt;/a&gt; is a good example. A developer came in with a well-structured request: they had an agentic document processing pipeline where several independent tasks (extracting text, analyzing images, classifying metadata) were all running sequentially. Each step was calling an LLM. The total latency was roughly the sum of all individual calls. The question was simple: can these branches run in parallel?&lt;/p&gt;

&lt;p&gt;It's the kind of request that feels obvious in hindsight. Of course they should run in parallel when there's no dependency between them. But when you're in the middle of building a framework, you're thinking about the happy path, the core abstraction, the learning curve. Edge cases — even smart ones — come later. This was one of those cases where a user saw the full potential of the architecture before I had fully mapped it myself.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Parallel Branches work
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://docs.neuron-ai.dev/workflow/loops-and-branches#parallel-branches" rel="noopener noreferrer"&gt;Workflow&lt;/a&gt; in Neuron AI is event-driven. Each node receives an event, does its work, and returns an event that determines which node runs next.&lt;/p&gt;

&lt;p&gt;Sequential pipelines, loops, conditional branches, all of that emerges from which events a node declares as its input and return types. Parallel execution fits naturally into this model.&lt;/p&gt;

&lt;p&gt;When a node needs to fan out into multiple independent branches, it returns a &lt;code&gt;ParallelEvent&lt;/code&gt; instead of a regular event. You pass an array of branch name and first-event pairs to its constructor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;NeuronAI\Workflow\Events\ParallelEvent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DocumentProcessing&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Node&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;StartEvent&lt;/span&gt; &lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;WorkflowState&lt;/span&gt; &lt;span class="nv"&gt;$state&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;ParallelEvent&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ParallelEvent&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
            &lt;span class="s1"&gt;'text'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextProcessEvent&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="s1"&gt;'image'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ImageProcessEvent&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each branch is a named key mapping to the first event of that branch. The nodes that handle those events, and all subsequent nodes in each branch, are registered in the workflow as usual:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyWorkflow&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Workflow&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DocumentProcessing&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;

            &lt;span class="c1"&gt;// "text" branch&lt;/span&gt;
            &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DescriptionGenerationNode&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextRefactorNode&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;

            &lt;span class="c1"&gt;// "image" branch&lt;/span&gt;
            &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ImageProcessNode&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AddWatermarkNode&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;

            &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;MergeNode&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each branch ends when its last node returns a StopEvent. The StopEvent can carry a result payload, which is how data flows back from the branches to the main workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TextRefactorNode&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Node&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;TextProcessEvent&lt;/span&gt; &lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;WorkflowState&lt;/span&gt; &lt;span class="nv"&gt;$state&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;StopEvent&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// ... do the work&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;StopEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$refinedText&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once all branches have completed, the ParallelEvent is forwarded to the next node — the merge point. That node receives the &lt;code&gt;ParallelEvent&lt;/code&gt; and can read each branch's result by name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MergeNode&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Node&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;ParallelEvent&lt;/span&gt; &lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;WorkflowState&lt;/span&gt; &lt;span class="nv"&gt;$state&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;StopEvent&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$textResult&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'text'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nv"&gt;$imageResult&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'image'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Combine, persist, return a final event...&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;StopEvent&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One detail worth noting: each branch gets an isolated copy of the workflow state. They start with the same snapshot, but mutations inside a branch don't propagate to sibling branches or to the main workflow. The only way to pass data back is through the StopEvent result. This is intentional, it avoids a whole class of concurrency bugs where branches step on each other's state.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running Branches Concurrently
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcfnc3ljh9vj13ssvn3sl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcfnc3ljh9vj13ssvn3sl.png" alt="Neuron AI PHP Workflow Branches" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;By default, Neuron AI runs all nodes, including parallel branches, with its default WorkflowExecutor. The branches will still execute correctly, but in a sequential manner, just one after the other. For most use cases where the branches are lightweight, this is fine.&lt;/p&gt;

&lt;p&gt;If you want the branches to actually run at the same time, you need the &lt;code&gt;AsyncExecutor&lt;/code&gt;, which is built on &lt;a href="https://github.com/amphp/amp" rel="noopener noreferrer"&gt;Amp&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require amphp/amp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then override the &lt;code&gt;executor()&lt;/code&gt; method in your workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;NeuronAI\Workflow\Executor\AsyncExecutor&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyWorkflow&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Workflow&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;executor&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;WorkflowExecutorInterface&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AsyncExecutor&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;...&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To make the async executor actually useful for LLM calls inside nodes, you also need to use the AmpHttpClient when building your agents within those nodes. &lt;a href="https://docs.neuron-ai.dev/agent/async#amphttpclient" rel="noopener noreferrer"&gt;Neuron AI already provides it&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;NeuronAI\HttpClient\AmpHttpClient&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DescriptionGenerationNode&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Node&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;TextProcessEvent&lt;/span&gt; &lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;WorkflowState&lt;/span&gt; &lt;span class="nv"&gt;$state&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;StopEvent&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AsyncAgent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;make&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;UserMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Describe this image'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getMessage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;StopEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The performance difference is real. In the test suite, two branches each with a 100ms simulated delay complete in roughly 100ms with the AsyncExecutor, versus roughly 200ms when running sequentially. When you're dealing with actual LLM calls that take several seconds each, that gap becomes significant.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this is useful for
&lt;/h2&gt;

&lt;p&gt;The document processing scenario from issue &lt;a href="https://github.com/neuron-core/neuron-ai/issues/530" rel="noopener noreferrer"&gt;#530&lt;/a&gt; is the clearest example: you have a file, and you want to extract structured data from it while simultaneously generating a description. These two tasks don't depend on each other. There's no reason to wait for one before starting the other.&lt;/p&gt;

&lt;p&gt;The same pattern applies to any pipeline where independent enrichment steps need to converge before a final decision: running multiple agents with different specializations, fetching data from several sources in parallel before synthesizing a report, or evaluating a generated output across multiple dimensions simultaneously. The merge node is just a regular node, it can do whatever you need once it has all the branch results in hand.&lt;/p&gt;

&lt;p&gt;Full documentation is available at &lt;a href="https://docs.neuron-ai.dev" rel="noopener noreferrer"&gt;https://docs.neuron-ai.dev&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>php</category>
      <category>ai</category>
      <category>agents</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How to Stop a Streamed AI Response Mid-Flight in Neuron AI v3</title>
      <dc:creator>Valerio</dc:creator>
      <pubDate>Tue, 14 Apr 2026 11:00:59 +0000</pubDate>
      <link>https://dev.to/inspector/how-to-stop-a-streamed-ai-response-mid-flight-in-neuron-ai-v3-7c0</link>
      <guid>https://dev.to/inspector/how-to-stop-a-streamed-ai-response-mid-flight-in-neuron-ai-v3-7c0</guid>
      <description>&lt;p&gt;One thing I didn't anticipate when building &lt;a href="https://neuron-ai.dev/" rel="noopener noreferrer"&gt;Neuron AI&lt;/a&gt; was how many edge cases would surface not from the AI integration itself, but from the UI layer sitting on top of it. Developers don't just want agents that work. They want agents that feel right to use. And the moment you start building chat interfaces on top of streaming responses, you quickly realize that “feeling right” involves a lot of details that never show up in framework documentation.&lt;/p&gt;

&lt;p&gt;A developer posted a &lt;a href="https://github.com/neuron-core/neuron-ai/discussions/444" rel="noopener noreferrer"&gt;new discussion&lt;/a&gt; on the Neuron AI GitHub repository asking about something deceptively simple: how do you let a user stop a streaming response while it's still in progress?&lt;/p&gt;

&lt;p&gt;You ask a question, the model starts generating a long answer, you realize mid-way through that it's going in the wrong direction, and you want to interrupt it. ChatGPT has a stop button. Claude has one too. From a user experience standpoint it's a small thing. From an implementation standpoint, it's trickier than it looks.&lt;/p&gt;

&lt;p&gt;The developer, had already built a reasonable first attempt. His approach was to set a cache key when the user clicked stop, then check that key inside the streaming loop before processing each chunk. The code was clean and the idea was sound:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;newMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;Chat&lt;/span&gt; &lt;span class="nv"&gt;$chat&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;StreamedResponse&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Cache&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;forget&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"chat_stop_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$chat&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;StreamedResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$chat&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;UserMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

        &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$stream&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$chunk&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Cache&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"chat_stop_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$chat&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nc"&gt;Cache&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;forget&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"chat_stop_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$chat&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'data: '&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;json_encode&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'status'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'stopped'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="nb"&gt;flush&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

            &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'data: '&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;json_encode&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'content'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$chunk&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nb"&gt;flush&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The problem was that breaking out of the loop early bypassed the part of the framework responsible for saving the AssistantMessage to chat history. The message was never persisted, leaving the conversation state inconsistent. He asked whether there was a recommended way to handle this properly, and whether partial responses could be saved on interruption.&lt;/p&gt;

&lt;p&gt;My initial answer was honest about the state of things: in earlier versions of the framework it would have been difficult to hook deeply enough into the streaming mechanism to solve this cleanly. But with v3, the architecture changed in a way that made this kind of customization straightforward. Every AI provider now accepts an injectable HTTP client, which means you can intercept the connection at a lower level than the application loop.&lt;/p&gt;

&lt;p&gt;The developer upgraded to v3, read the &lt;a href="https://docs.neuron-ai.dev/overview/upgrade" rel="noopener noreferrer"&gt;upgrade guide&lt;/a&gt;, and came back a few days later with a solution that I think deserves to be documented properly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architecture Behind the Fix
&lt;/h2&gt;

&lt;p&gt;In Neuron AI v3, every provider component accepts an &lt;code&gt;httpClient&lt;/code&gt; parameter. By default the framework uses &lt;code&gt;GuzzleHttpClient&lt;/code&gt;, but you can pass in any class that implements &lt;code&gt;HttpClientInterface&lt;/code&gt;. This was introduced primarily to support async execution, but it opens the door to a wider range of customizations, this being a good example.&lt;/p&gt;

&lt;p&gt;The key insight is that streaming in the framework is not just a PHP foreach loop iterating over chunks. Under the hood, the provider calls &lt;code&gt;$httpClient-&amp;gt;stream()&lt;/code&gt;, which returns a StreamInterface object. The framework then reads from that stream until &lt;code&gt;eof()&lt;/code&gt; returns true. This means that if you control the StreamInterface implementation, you control when the stream ends, from the framework's perspective.&lt;/p&gt;

&lt;p&gt;The developer implemented two classes: &lt;code&gt;StoppableHttpClient&lt;/code&gt; and &lt;code&gt;StoppableStream&lt;/code&gt;. The HTTP client is a decorator around the default Guzzle client. It delegates all standard operations to the inner client but intercepts the &lt;code&gt;stream()&lt;/code&gt; call to wrap the result in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;StoppableHttpClient&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;HttpClientInterface&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$chatId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;HttpClientInterface&lt;/span&gt; &lt;span class="nv"&gt;$inner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;GuzzleHttpClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;HttpRequest&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;HttpResponse&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;inner&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;HttpRequest&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;StreamInterface&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;StoppableStream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;inner&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;chatId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;withHeaders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$headers&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;HttpClientInterface&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;chatId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;inner&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;withHeaders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$headers&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;withBaseUri&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$baseUri&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;HttpClientInterface&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;chatId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;inner&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;withBaseUri&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$baseUri&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;withTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="nv"&gt;$timeout&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;HttpClientInterface&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;chatId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;inner&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;withTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$timeout&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice that &lt;code&gt;withHeaders&lt;/code&gt;, &lt;code&gt;withBaseUri&lt;/code&gt;, and &lt;code&gt;withTimeout&lt;/code&gt; all return a new self instance, preserving the decorator pattern and keeping the &lt;code&gt;chatId&lt;/code&gt; context intact through any configuration the framework might apply to the client internally.&lt;/p&gt;

&lt;p&gt;The real logic lives in &lt;code&gt;StoppableStream&lt;/code&gt;. Its &lt;code&gt;eof()&lt;/code&gt; method does something simple but effective: before delegating to the inner stream, it checks a cache key. If the key is set, it marks itself as stopped, clears the flag, closes the underlying connection, and returns true. Returning true from &lt;code&gt;eof()&lt;/code&gt; signals to the framework that the stream has ended naturally, which means the normal post-stream behavior runs, including saving the partial AssistantMessage to history:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;StoppableStream&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;StreamInterface&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nv"&gt;$stopped&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;StreamInterface&lt;/span&gt; &lt;span class="nv"&gt;$inner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$chatId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;eof&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;stopped&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Cache&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"chat_stop_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;chatId&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;stopped&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nc"&gt;Cache&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;forget&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"chat_stop_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;chatId&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;inner&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;inner&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;eof&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$length&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;inner&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$length&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;readLine&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;inner&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;readLine&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;close&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;inner&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;$stopped&lt;/code&gt; flag is a small but important detail. Once the stream decides it's done, subsequent calls to &lt;code&gt;eof()&lt;/code&gt; return true immediately without touching the cache again. This avoids any ambiguity if the framework calls &lt;code&gt;eof()&lt;/code&gt; multiple times after the interruption.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wiring It Into the Agent
&lt;/h2&gt;

&lt;p&gt;Injecting the custom client into an agent is exactly as straightforward as you'd expect. You pass the chatId into the agent constructor and use it when building the provider:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BlogAgent&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Agent&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$chatId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;parent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;AIProviderInterface&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Gemini&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'services.gemini.api_key'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'gemini-3-flash-preview'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;httpClient&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;StoppableHttpClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;chatId&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;chatHistory&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;ChatHistoryInterface&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;EloquentChatHistory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;threadId&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;chatId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;modelClass&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ChatMessage&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;contextWindow&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;75000&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;instructions&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;SystemPrompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;background&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;BlogAgentPrompt&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;getBackgroundPrompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;chatId&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;BlogAgentPrompt&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;OUTPUT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the HTTP side, the stop endpoint remains exactly what the developer originally had: a simple controller action that sets the cache key, which the &lt;code&gt;StoppableStream&lt;/code&gt; will pick up on its next &lt;code&gt;eof()&lt;/code&gt; check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Chat&lt;/span&gt; &lt;span class="nv"&gt;$chat&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;JsonResponse&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Cache&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"chat_stop_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$chat&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;response&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'status'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'stopping'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why This Approach Works Well
&lt;/h2&gt;

&lt;p&gt;What I find most interesting about this solution is where the logic lives. The developer's original approach tried to solve the problem at the application loop level, after the stream had already been read by the framework. This created the side effect of bypassing history persistence. By moving the logic down into the &lt;code&gt;StreamInterface&lt;/code&gt; layer, the interruption becomes invisible to the rest of the framework. As far as Neuron AI is concerned, the stream simply ended. Everything that normally happens at the end of a stream still happens.&lt;/p&gt;

&lt;p&gt;It also keeps the agent class clean. The &lt;code&gt;BlogAgent&lt;/code&gt; doesn't know or care that its stream can be stopped externally. That concern lives entirely in &lt;code&gt;StoppableHttpClient&lt;/code&gt; and &lt;code&gt;StoppableStream&lt;/code&gt;, which can be reused across any agent in the application that needs the same behavior.&lt;/p&gt;

&lt;p&gt;After seeing this solution I noted is a well-composed solution that works cleanly within the existing extension points v3 provides.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;p&gt;If you are getting started with AI Agents, or you simply want to elevate your skills to a new level here is a list of resources to help you go in the right direction:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Repository: &lt;a href="https://github.com/inspector-apm/neuron-ai" rel="noopener noreferrer"&gt;https://github.com/inspector-apm/neuron-ai&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Newsletter: &lt;a href="https://neuron-ai.dev" rel="noopener noreferrer"&gt;https://neuron-ai.dev&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;E-Book (Start With AI Agents In PHP): &lt;a href="https://www.amazon.com/dp/B0F1YX8KJB" rel="noopener noreferrer"&gt;https://www.amazon.com/dp/B0F1YX8KJB&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>php</category>
      <category>agents</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Conversational Data Collection: Introducing AIForm for Neuron AI</title>
      <dc:creator>Valerio</dc:creator>
      <pubDate>Wed, 01 Apr 2026 10:48:01 +0000</pubDate>
      <link>https://dev.to/inspector/conversational-data-collection-introducing-aiform-for-neuron-ai-57c</link>
      <guid>https://dev.to/inspector/conversational-data-collection-introducing-aiform-for-neuron-ai-57c</guid>
      <description>&lt;p&gt;One of the more interesting things about building an open-source framework is that the community often knows what to build next before you do. When I started Neuron AI, I had a fairly clear picture in my head of the core primitives — agents, tools, workflows, structured output. What I didn’t fully anticipate was how quickly developers would start pushing those primitives toward very specific, practical use cases. The feature requests and questions that come through GitHub and the newsletter are often the most honest signal I have about where real-world PHP developers are actually trying to go.&lt;/p&gt;

&lt;p&gt;Over the past few months, one request kept surfacing in different forms: how do I use Neuron AI to collect information from a user through a conversation instead of a traditional form? The details varied — a registration flow here, a support intake there, a booking assistant somewhere else — but the underlying need was the same. People were trying to build this themselves on top of the agent and workflow components, and it was working, but it required a non-trivial amount of plumbing.&lt;/p&gt;

&lt;p&gt;So today I'm releasing &lt;a href="https://github.com/neuron-core/ai-form" rel="noopener noreferrer"&gt;&lt;strong&gt;AIForm&lt;/strong&gt;&lt;/a&gt;, a Neuron AI component that handles exactly this use case.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;p&gt;The idea is straightforward. You define the data you want to collect as a plain PHP class using the same &lt;code&gt;#[SchemaProperty]&lt;/code&gt; attributes you already know from Neuron AI's structured output system. You attach validation rules where needed. Then you extend AIForm, wire up a provider, and define what should happen when the form completes. The component takes care of the rest: managing the conversation across multiple turns, tracking which fields have been collected, retrying on validation failures, and calling your callback once everything is in order.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RegistrationData&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;#[SchemaProperty(description: 'User full name', required: true)]&lt;/span&gt;
    &lt;span class="na"&gt;#[NotBlank]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="na"&gt;#[SchemaProperty(description: 'Email address', required: true)]&lt;/span&gt;
    &lt;span class="na"&gt;#[Email]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$email&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="na"&gt;#[SchemaProperty(description: 'Phone number')]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;?string&lt;/span&gt; &lt;span class="nv"&gt;$phone&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RegistrationForm&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;AIForm&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$formDataClass&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RegistrationData&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;AIProviderInterface&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Anthropic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'ANTHROPIC_API_KEY'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'ANTHROPIC_MODEL'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;mixed&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;RegistrationData&lt;/span&gt; &lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;userService&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In a controller, you process each incoming message through the form instance and get back a state object telling you the current status, completion percentage, and any missing fields, everything you need to drive the UI.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RegistrationForm&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;make&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setChatHistory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;FileChatHistory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"/tmp/chats/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$sessionId&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="nv"&gt;$handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$form&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;UserMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'message'&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
&lt;span class="nv"&gt;$state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$handler&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;response&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="s1"&gt;'status'&lt;/span&gt;          &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$state&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getStatus&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'message'&lt;/span&gt;         &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$handler&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getLastResponse&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="s1"&gt;'completion'&lt;/span&gt;      &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$state&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getCompletionPercentage&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="s1"&gt;'missing_fields'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$state&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getMissingFields&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="s1"&gt;'is_complete'&lt;/span&gt;     &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$form&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;isComplete&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The confirmation step
&lt;/h2&gt;

&lt;p&gt;Something that came up during the build was the question of what to do once all required data is collected but before the callback fires. In many real flows: registrations, bookings, checkout-adjacent interactions, you want the user to review what was collected and confirm before anything is committed. AIForm handles this with &lt;code&gt;requireConfirmation()&lt;/code&gt;, which causes the workflow to throw a FormInterruptRequest instead of submitting immediately. You can serialize that interrupt into the session, present the AI-generated summary to the user, and then resume the workflow with the user's response. If they confirm, the callback runs. If they want to change something, the form drops back into collection mode.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RegistrationForm&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;make&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;requireConfirmation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nv"&gt;$handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$form&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;UserMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'message'&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
&lt;span class="nv"&gt;$state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$handler&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$handler&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getInterrupt&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nc"&gt;FormInterruptRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$_SESSION&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'form_interrupt'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;serialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$handler&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getInterrupt&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="c1"&gt;// return the AI-generated summary to the user&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// on the next request, resume:&lt;/span&gt;
&lt;span class="nv"&gt;$interrupt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;unserialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$_SESSION&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'form_interrupt'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="nv"&gt;$handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$form&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;UserMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'message'&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="nv"&gt;$interrupt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is built on the same human-in-the-loop interruption mechanism introduced in NeuronAI v2 workflows, so the pattern will feel familiar if you've used that before.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters beyond the obvious
&lt;/h2&gt;

&lt;p&gt;The instinct when you first see this is to think of it as a chatbot wrapper around a form. That's a reasonable first impression, but it slightly misses the point. The more interesting aspect is what happens with users who don't fill in forms cleanly: they skip optional fields, mistype emails, answer questions out of order, or abandon the flow halfway through because the interface felt rigid. A conversational flow handles all of this naturally — the AI asks follow-up questions, flags validation failures in plain language, and keeps state across turns without you writing any of that logic yourself.&lt;/p&gt;

&lt;p&gt;There's also a less obvious benefit for mobile and voice-adjacent interfaces, where long forms are genuinely painful to use. If your PHP application already has an API layer and a frontend consuming it, plugging AIForm into a controller gives you a conversation endpoint you can connect to any interface — including ones that don't have keyboard input as their primary interaction model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Built on the existing stack
&lt;/h2&gt;

&lt;p&gt;AIForm is a NeuronAI workflow under the hood, which means it inherits everything the workflow system already provides. That includes native Inspector integration, if you have &lt;code&gt;INSPECTOR_INGESTION_KEY&lt;/code&gt; set in your environment, every form conversation will appear in your Inspector dashboard with the full execution timeline. This matters in production when a conversation stalls or a validation loop behaves unexpectedly and you need to understand exactly what happened.&lt;/p&gt;

&lt;p&gt;The package is available now on GitHub at &lt;code&gt;neuron-core/ai-form&lt;/code&gt;. Install it with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require neuron-ai/ai-form
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Documentation for the structured output system and workflow components that AIForm builds on is at docs.neuron-ai.dev. If you build something with it, I'd genuinely like to hear about the use case — the community feedback is what shaped this component in the first place.&lt;/p&gt;

</description>
      <category>php</category>
      <category>webdev</category>
      <category>ai</category>
      <category>backend</category>
    </item>
    <item>
      <title>Neuron AI Now Supports ZAI — The GLM Series Is Worth Your Attention</title>
      <dc:creator>Valerio</dc:creator>
      <pubDate>Mon, 23 Mar 2026 15:10:17 +0000</pubDate>
      <link>https://dev.to/ilvalerione/neuron-ai-now-supports-zai-the-glm-series-is-worth-your-attention-6ek</link>
      <guid>https://dev.to/ilvalerione/neuron-ai-now-supports-zai-the-glm-series-is-worth-your-attention-6ek</guid>
      <description>&lt;p&gt;There's a pattern I’ve noticed over the past year while working on Neuron AI: the decisions that matter most are rarely about chasing trends. They’re about quietly recognizing something that works, testing it seriously, and integrating it so that other developers can benefit without having to do that work themselves.&lt;/p&gt;

&lt;p&gt;That’s the honest story behind adding &lt;a href="https://z.ai/" rel="noopener noreferrer"&gt;&lt;strong&gt;ZAI&lt;/strong&gt;&lt;/a&gt; platform integration to &lt;a href="https://neuron-ai.dev/" rel="noopener noreferrer"&gt;Neuron AI&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I started evaluating the GLM models, because the numbers made me curious. GLM-5 is a 744 billion parameter Mixture-of-Experts model, with 40 billion parameters active per token, trained on 28.5 trillion tokens. For context: it currently holds the top spot among open-weight models on several banchmarks, and places competitively against frontier closed models on reasoning tasks. You can read more about their amazing job in this article: &lt;a href="https://z.ai/blog/glm-5" rel="noopener noreferrer"&gt;https://z.ai/blog/glm-5&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It’s also open-weight. There's a real case to be made that GLM-5 represents the most capable open model available right now, and that matters if you care about the ability to self-host, audit, and control what runs inside your applications.&lt;/p&gt;

&lt;p&gt;I tested it. It earned its place in the framework.&lt;/p&gt;

&lt;h2&gt;
  
  
  A note on ZAI
&lt;/h2&gt;

&lt;p&gt;This integration is also the beginning of a collaboration with &lt;a href="https://z.ai/" rel="noopener noreferrer"&gt;ZAI&lt;/a&gt;. They've offered to help spread the word about Neuron within their developer community, which I'm genuinely grateful for. The PHP ecosystem sometimes feels isolated from the broader AI conversation — most of the tooling, the tutorials, and the discourse assumes Python. Partnerships like this one help change that. I want Neuron to be where PHP developers go when they're serious about building agentic systems, and having ZAI’s support is a meaningful step in that direction. More will follow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using ZAI as an AI Provider
&lt;/h2&gt;

&lt;p&gt;The core integration is straightforward. You swap in the ZAI provider and you're done — all of Neuron's agentic capabilities, including tools, streaming, structured output, middleware, MCP connectors, and RAG, work exactly as they do with any other provider.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Advanced Reasoning Models&lt;/strong&gt;: Unlocking complex, multi-step agentic workflows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multimodal Input &amp;amp; Output&lt;/strong&gt;: Allowing agents to natively perceive and interact with the world.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Image Generation&lt;/strong&gt;: Giving your agents the ability to create visual assets on the fly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audio Transcription&lt;/strong&gt;: Bridging the gap between voice and agentic action.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Neuron&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;NeuronAI\Agent\Agent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;NeuronAI\Chat\Messages\UserMessage&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;NeuronAI\Providers\AIProviderInterface&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="no"&gt;NeuronAI\Providers\ZAI\ZAI&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyAgent&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Agent&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;AIProviderInterface&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ZAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'ZAI_API_KEY'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'glm-5'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nv"&gt;$message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MyAgent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;make&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;UserMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Analyze the performance bottleneck in this query..."&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getMessage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$message&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getContent&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The GLM series also supports reasoning models. If you want the model to think through a problem before responding, you pass the appropriate parameters through the parameters array, as you would with any other provider in Neuron.&lt;/p&gt;

&lt;p&gt;Full documentation: docs.neuron-ai.dev/providers/ai-provider#zai&lt;/p&gt;

&lt;h2&gt;
  
  
  Image Generation
&lt;/h2&gt;

&lt;p&gt;ZAI exposes an image generation endpoint through the glm-image model. In Neuron, this is implemented as a dedicated &lt;code&gt;ZAIImage&lt;/code&gt; provider that implements the same &lt;code&gt;AIProviderInterface&lt;/code&gt; used by all other providers. This means you can drop it directly into an agent, and it participates in the same middleware and workflow infrastructure.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Neuron&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;NeuronAI\Agent\Agent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;NeuronAI\Chat\Messages\UserMessage&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;NeuronAI\Providers\AIProviderInterface&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;NeuronAI\Providers\ZAI\Image\ZAIImage&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ImageAgent&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Agent&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;AIProviderInterface&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ZAIImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'ZAI_API_KEY'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'glm-image'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nv"&gt;$message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ImageAgent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;make&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;UserMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Generate a diagram of a distributed caching architecture"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getMessage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Returns the URL of the generated image&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$message&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getImage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getContent&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you prefer to use the provider directly, without wrapping it in an agent class, that’s supported too:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;NeuronAI\Providers\ZAI\Image\ZAIImage&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nv"&gt;$provider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ZAIImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'ZAI_API_KEY'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'glm-image'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nv"&gt;$message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$provider&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;UserMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"A technical illustration of an event-driven system"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$message&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getImage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getContent&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Full documentation: docs.neuron-ai.dev/providers/image#zai-image&lt;/p&gt;

&lt;h2&gt;
  
  
  Audio Transcription
&lt;/h2&gt;

&lt;p&gt;The ZAI audio integration covers speech-to-text transcription via the glm-asr-2512 model. The pattern is consistent with how all audio providers work in Neuron: you pass an &lt;code&gt;AudioContent&lt;/code&gt; object inside a &lt;code&gt;UserMessage&lt;/code&gt;, and the provider returns a standard message whose text content contains the transcription.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;NeuronAI\Providers\ZAI\Audio\ZAITranscription&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;NeuronAI\Chat\Messages\UserMessage&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;NeuronAI\Chat\Messages\Content\AudioContent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;NeuronAI\Chat\Messages\Content\SourceType&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nv"&gt;$provider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ZAITranscription&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'ZAI_API_KEY'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'glm-asr-2512'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nv"&gt;$message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$provider&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;UserMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AudioContent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;__DIR__&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'/assets/recording.mp3'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;SourceType&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$message&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getContent&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This fits naturally into workflows that involve voice input, meeting transcription, or any pipeline where audio needs to feed into an agent. Because the transcription provider implements the standard &lt;code&gt;AIProviderInterface&lt;/code&gt;, you can also embed it inside a larger agentic workflow and apply middleware — rate limiting, logging, guardrails — the same way you would anywhere else in Neuron.&lt;/p&gt;

&lt;p&gt;Full documentation: docs.neuron-ai.dev/providers/audio#zai&lt;/p&gt;

&lt;h2&gt;
  
  
  What comes next
&lt;/h2&gt;

&lt;p&gt;This is a first step. The collaboration with ZAI opens up the possibility of going deeper: more model capabilities, more joint content, more visibility for PHP developers who are building serious agentic applications. I’m not going to over-promise what that looks like in practice, but the intention on both sides is to keep building.&lt;/p&gt;

&lt;p&gt;If you’re already using Neuron, you can start experimenting with ZAI models today. The &lt;a href="https://docs.neuron-ai.dev/" rel="noopener noreferrer"&gt;full documentation&lt;/a&gt; covers everything you need. Feel free to open an issue on the Neuron repository or contribute if you find possible improvements: &lt;a href="https://github.com/neuron-core/neuron-ai" rel="noopener noreferrer"&gt;https://github.com/neuron-core/neuron-ai&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>ai</category>
      <category>php</category>
      <category>agents</category>
    </item>
  </channel>
</rss>
