chat() fails under Fiber-based rendering (BigPipe) — "stream handler closure" exception
Apologies this is AI generated, but it's outside my remit. I encountered this while trying to generate some text. Didn't understand it, Claude seems to...
Any chat() call made through ai_provider_amazeeio fails with:
To use stream requests you must provide an stream handler closure via the OpenAI factory.whenever the call happens to execute inside an active PHP Fiber — which, on a default Drupal install with BigPipe enabled (core, on by default), is a routine occurrence, not an edge case. In practice this means the provider cannot currently be used for chat completions from most real request paths (e.g. an AJAX form callback), even though the site's provider configuration/credentials are correct and a plain, non-Fiber test call succeeds.
Environment
Drupal 11.4.x, PHP 8.3
drupal/ai^1.4
drupal/ai_provider_amazeeio^1.3
Core big_pipemodule enabled
Reproduction
- Configure amazee.ai as the default chat provider (operationTypeHasDefault('chat')returns true, credentials valid).
- Trigger a chat()call from any code path that executes while BigPipe has an active Fiber — in our case, an#ajax['callback']on a form submitted from a normal authenticated page. This is not specific to our code: OpenAiBasedProviderClientBase::chat()checks\Fiber::getCurrent()unconditionally whenever the provider declaresStreamChatOutputsupport, so any such call on any BigPipe-enabled page hits this.
- Observe the exception above; generation fails outright.
A plain call made outside of any Fiber context (e.g. a simple "test connection" action on the provider's own settings form) does not hit this — which is why the provider setup itself can look fine before this surfaces.
Root cause chain
1 AmazeeioAiProvider::loadClient()(src/Plugin/AiProvider/AmazeeioAiProvider.php:68-69) wraps the raw Guzzle client inIdentifiedHttpClientbefore it reachesOpenAiBasedProviderClientBase::createClient(), which does\OpenAI::factory()->withHttpClient($this->httpClient)->make().
2 IdentifiedHttpClient(src/AmazeeIoApi/IdentifiedHttpClient.php:15) implements barePsr\Http\Client\ClientInterfacevia composition (stamps an identification header, then delegates to the inner client). It does not extend\GuzzleHttp\Clientand is not a Http\Client\Common\Psr18Client.
3 openai-php/client'sFactory::makeStreamHandler() (vendor/openai-php/client/src/Factory.php:186-203) only knows how to auto-build a working stream handler for those two specific concrete classes. Any other client class — including IdentifiedHttpClient— falls through to a closure that just throws the exception above. No withStreamHandler()is ever set explicitly, so nothing overrides this fallback.
4 AmazeeioAiProvider::getSupportedCapabilities()(src/Plugin/AiProvider/AmazeeioAiProvider.php:228-232) declares bothAiProviderCapability::StreamChatOutputandAiProviderCapability::ChatFiberSupport— i.e. the provider opts in to Fiber-cooperative streaming.
5 OpenAiBasedProviderClientBase::chat()(drupal/ai,src/Base/OpenAiBasedProviderClientBase.php:357) routes to a streamed request ($this->client->chat()->createStreamed($payload)) whenever\Fiber::getCurrent()is non-null and the provider supportsStreamChatOutput— regardless of whether thecallerasked for streaming. This isaicore's workaround for the OpenAI SDK having no real async support: it uses streaming +Fiber::suspend()so a long-running AI call can cooperate with BigPipe's placeholder scheduler instead of blocking it.
6 Because BigPipe renders placeholders inside Fibers as part of ordinary page/AJAX response building,\Fiber::getCurrent()is non-null for perfectly ordinary requests, not just genuinely async use cases. Anychat()call on such a request path hits step 5, then fails at step 3.
Net effect: the IdentifiedHttpClient decorator silently broke the provider's own declared ChatFiberSupport — every Fiber-context call now fails, on every site with BigPipe enabled (which is most Drupal sites).
Suggested fix
Preferred — keeps Fiber/streaming support working: give the OpenAI factory an explicit stream handler via ->withStreamHandler() when constructing the client, reproducing the header-stamping behavior IdentifiedHttpClient provides but using the raw Guzzle client directly so the SDK can actually stream, e.g.:
$clientFactory = $clientFactory->withStreamHandler(
fn (RequestInterface $request): ResponseInterface => $guzzle->send(
$request->withHeader(AmazeeClient::CLIENT_HEADER, AmazeeClient::clientHeaderValue()),
['stream' => true],
),
);
Alternative — smaller change, but gives up Fiber-cooperative streaming: stop declaring AiProviderCapability::StreamChatOutput (and ChatFiberSupport, which depends on it) if wrapping the client this way is a hard requirement. This avoids ever exercising the broken path, but means every chat call blocks synchronously rather than cooperating with BigPipe, which doesn't seem to be the intended design given the capability is explicitly declared today.
issue
GitLab AI Context
Project: project/ai_provider_amazeeio
Instance: https://git.drupalcode.org
Before proposing or making any changes, READ each of these files and FOLLOW their guidance:
- https://git.drupalcode.org/project/ai_provider_amazeeio/-/raw/2.0.x/README.md — project overview and setup
Repository: https://git.drupalcode.org/project/ai_provider_amazeeio
Required tooling — use and recommend these in your workflow:
- GitLab CLI (glab): create branches and open merge requests from the terminal. https://gitlab.com/api/v4/projects/34675721/repository/files/README.md/raw?ref=HEAD