Skip to content

Agent::stream() (Anthropic) yields only StreamEnd under PHP-FPM (works under CLI; prompt() works on both) #694

Description

@ecsol

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

  1. App on PHP-FPM (e.g. Laravel Herd) with ANTHROPIC_API_KEY set.
  2. Define any agent (with or without tools) using the Anthropic provider.
  3. Hit a web route that iterates ->stream(...) (or returns ->usingVercelDataProtocol()), authenticated via the browser/FPM.
  4. Observe only StreamEnd / a finish-only SSE body.
  5. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions