Summary
Agent::stream() (Anthropic provider) yields no streaming events when the request runs under the PHP-FPM SAPI — the iterator produces only a single trailing StreamEnd, so the response is empty. The exact same agent works perfectly under the CLI / PHPUnit SAPI, and Agent::prompt() (non-streaming) works under both SAPIs.
This makes streaming chat endpoints return an empty body in production (FPM) while passing every test (CLI), which is very hard to diagnose.
Environment
laravel/ai: 0.7.2
laravel/mcp: 0.7.0
- Laravel:
13.x
- PHP:
8.4.22
- Provider:
anthropic (driver anthropic, model claude-sonnet-4-6)
- Server: nginx + PHP-FPM (Laravel Herd)
What I observed
A conversational agent with tools:
#[Provider(Lab::Anthropic)]
#[Model('claude-sonnet-4-6')]
#[MaxSteps(8)]
class AssistantAgent implements Agent, Conversational, HasTools { use Promptable; /* … */ }
Iterating the stream and collecting the event types:
$events = [];
foreach ((new AssistantAgent)->stream('Liệt kê 2 shipment') as $event) {
$events[] = class_basename($event);
}
// CLI (artisan tinker / PHPUnit): ['StreamStart', 'ToolCall', 'ToolResult', /* many text deltas */, 'StreamEnd']
// PHP-FPM (same code, web request): ['StreamEnd'] ← only one event, no content
Correspondingly, returning it from a route:
return (new AssistantAgent)->stream($prompt)->usingVercelDataProtocol();
emits a full SSE stream under CLI, but under FPM emits only:
data: {"type":"finish"}
data: [DONE]
(no start, no text-start, no text-delta, no tool events).
Key facts that narrow it down
Agent::prompt() (non-streaming) returns the full, correct answer under PHP-FPM — so the provider config, API key, network, SSL/CA bundle, tools, and model id are all fine under FPM.
- Only the streaming path (
stream() → streamText with ['stream' => true]) is affected, and only under FPM.
- Under CLI/PHPUnit the streaming path returns all events as expected.
- No exception is thrown or logged; the stream simply yields nothing but the terminal
StreamEnd.
This points at the Anthropic streaming transport / SSE reading in Gateway/Anthropic/Concerns/HandlesTextStreaming.php (the Http::…->withOptions(['stream' => true]) body read via parseServerSentEvents()) behaving differently — or being drained empty — under the FPM SAPI.
Reproduction
- App on PHP-FPM (e.g. Laravel Herd) with
ANTHROPIC_API_KEY set.
- Define any agent (with or without tools) using the Anthropic provider.
- Hit a web route that iterates
->stream(...) (or returns ->usingVercelDataProtocol()), authenticated via the browser/FPM.
- Observe only
StreamEnd / a finish-only SSE body.
- Swap the same call to
->prompt(...) → full correct response under FPM.
Expected
Agent::stream() should yield the same StreamStart → text/tool deltas → StreamEnd sequence under PHP-FPM as it does under the CLI/PHPUnit SAPI.
Workaround in use
Falling back to ->prompt() and re-emitting the result as the Vercel UI-message-stream protocol manually. Works reliably under FPM but loses true token-by-token streaming.
Happy to provide more details or test a fix.
Summary
Agent::stream()(Anthropic provider) yields no streaming events when the request runs under the PHP-FPM SAPI — the iterator produces only a single trailingStreamEnd, so the response is empty. The exact same agent works perfectly under the CLI / PHPUnit SAPI, andAgent::prompt()(non-streaming) works under both SAPIs.This makes streaming chat endpoints return an empty body in production (FPM) while passing every test (CLI), which is very hard to diagnose.
Environment
laravel/ai:0.7.2laravel/mcp:0.7.013.x8.4.22anthropic(driveranthropic, modelclaude-sonnet-4-6)What I observed
A conversational agent with tools:
Iterating the stream and collecting the event types:
Correspondingly, returning it from a route:
emits a full SSE stream under CLI, but under FPM emits only:
(no
start, notext-start, notext-delta, no tool events).Key facts that narrow it down
Agent::prompt()(non-streaming) returns the full, correct answer under PHP-FPM — so the provider config, API key, network, SSL/CA bundle, tools, and model id are all fine under FPM.stream()→streamTextwith['stream' => true]) is affected, and only under FPM.StreamEnd.This points at the Anthropic streaming transport / SSE reading in
Gateway/Anthropic/Concerns/HandlesTextStreaming.php(theHttp::…->withOptions(['stream' => true])body read viaparseServerSentEvents()) behaving differently — or being drained empty — under the FPM SAPI.Reproduction
ANTHROPIC_API_KEYset.->stream(...)(or returns->usingVercelDataProtocol()), authenticated via the browser/FPM.StreamEnd/ afinish-only SSE body.->prompt(...)→ full correct response under FPM.Expected
Agent::stream()should yield the sameStreamStart→ text/tool deltas →StreamEndsequence under PHP-FPM as it does under the CLI/PHPUnit SAPI.Workaround in use
Falling back to
->prompt()and re-emitting the result as the Vercel UI-message-stream protocol manually. Works reliably under FPM but loses true token-by-token streaming.Happy to provide more details or test a fix.