Chat interfaces people are used to, ChatGPT, Claude.ai, tend to show a reply appearing
word by word rather than snapping into view all at once. It’s tempting to assume
wp_ai_client_prompt() supports that out of the box. It doesn’t, not yet, and building
toward that assumption will leave you debugging a feature that was never there. This
lesson is honest about exactly what’s possible today, and gives you two real patterns:
a simple one that fakes the visual effect client-side, and a more advanced one for teams
that need genuine server-to-client streaming right now.
Lesson 4’s working chat endpoint and transcript handling. This lesson only changes how the reply is delivered and displayed, not how it’s generated.
Step 1: what the SDK actually does today
As of this writing, the PHP AI Client SDK’s generate_text() call is request-response:
it waits for the model provider to finish generating, then returns the complete text in
one piece. Token-by-token streaming is tracked as roadmap work for a future SDK release
(see the project’s own development roadmap discussion), it is not available in the
current SDK. Any code claiming wp_ai_client_prompt() streams a response today is
describing a feature that doesn’t exist yet, don’t build against that assumption.
That single fact is why every prior lesson’s endpoint returns one JSON object with the
whole reply in it, {"reply": "..."}, not a stream of partial chunks. What you build on
top of that fact is a choice between two honest patterns.
Step 2: pattern (a), fake it client-side, the recommended default
The generation itself still happens as one call, the same endpoint from Lesson 4, unchanged. What changes is display: once the full reply arrives, the browser reveals it progressively instead of dumping it in all at once.
// File: assets/js/chat-widget.js
function revealTextProgressively( element, fullText, charsPerTick ) {
var i = 0;
element.textContent = '';
var interval = setInterval( function () {
i += charsPerTick;
element.textContent = fullText.slice( 0, i );
if ( i >= fullText.length ) {
clearInterval( interval );
}
}, 15 );
}
// Inside the existing fetch().then() handler from Lesson 3/4:
.then( function ( data ) {
var bubble = document.createElement( 'div' );
bubble.className = 'site-chat-message site-chat-message--assistant';
messages.appendChild( bubble );
revealTextProgressively( bubble, data.reply || 'Sorry, something went wrong.', 3 );
} )
This is a legitimate, honest UX pattern, not a trick played on the visitor. The generation already finished server-side before this code runs, the animation is purely cosmetic polish that matches the feel visitors expect from other chat products, without claiming the backend does something it doesn’t.
Step 3: pattern (b), genuine streaming, bypassing the SDK for that one call
Some teams need the reply to actually start appearing before the full generation finishes, a longer response on a slow connection, for instance, where waiting for the complete text before showing anything feels sluggish. Real token-by-token delivery requires calling the AI provider’s own HTTP API directly for that specific request, outside the PHP AI Client SDK, since the SDK doesn’t expose a streaming method to call through.
The shape of it: your REST callback opens a streaming HTTP request to the provider
(most providers’ chat completion APIs support a stream: true parameter that returns
chunked, incremental output), reads each chunk as it arrives, and relays it to the
browser over Server-Sent Events as your own PHP script flushes output incrementally.
// File: inc/rest-chat-stream-endpoint.php
// Conceptual sketch, provider-specific request details vary by vendor.
add_action( 'rest_api_init', function () {
register_rest_route( 'my-plugin/v1', '/chat-stream', array(
'methods' => 'POST',
'callback' => 'my_plugin_handle_chat_stream',
'permission_callback' => 'my_plugin_chat_permission_check',
) );
} );
function my_plugin_handle_chat_stream( WP_REST_Request $request ) {
header( 'Content-Type: text/event-stream' );
header( 'Cache-Control: no-cache' );
$message = sanitize_textarea_field( $request->get_param( 'message' ) );
// This call goes directly to the provider's own streaming HTTP endpoint,
// using your stored API key server-side, not through wp_ai_client_prompt().
my_plugin_stream_from_provider( $message, function ( $chunk ) {
echo "data: " . wp_json_encode( array( 'chunk' => $chunk ) ) . "\n\n";
flush();
} );
echo "data: [DONE]\n\n";
flush();
exit;
}
// File: assets/js/chat-widget.js
// Conceptual sketch: reading a Server-Sent Events style response incrementally.
fetch( siteChatWidget.streamUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': siteChatWidget.nonce },
body: JSON.stringify( { message: text } ),
} ).then( function ( response ) {
var reader = response.body.getReader();
var decoder = new TextDecoder();
function read() {
reader.read().then( function ( result ) {
if ( result.done ) {
return;
}
appendChunkToLastMessage( decoder.decode( result.value ) );
read();
} );
}
read();
} );
Pattern (b) works, but every part of it, the request format, the chunk shape, how the stream ends, is defined by whichever AI provider you’re calling, not by any WordPress API. Switching providers means rewriting the streaming logic. It also means that one specific call bypasses the PHP AI Client SDK’s abstraction entirely, so you lose the SDK’s provider-switching convenience for that request. Only take this on if genuine token-by-token delivery is a hard product requirement today.
Step 4: choosing between them
Test it: confirm pattern (a) behaves correctly
Send a longer message that produces a multi-sentence reply. Confirm the text appears
progressively rather than all at once, and confirm the network tab still shows exactly
one request to /wp-json/my-plugin/v1/chat that completes before any animation starts,
proving the “streaming” is purely a client-side reveal, not a second network mechanism.
Recap
The PHP AI Client SDK generates a complete response in one call today, it does not stream tokens, and that’s expected to change only in a future SDK release. Pattern (a), generating the full reply once and revealing it progressively in the browser, is an honest, simple, provider-agnostic default that matches the UX visitors expect. Pattern (b), calling a provider’s own streaming HTTP API directly and relaying chunks over Server-Sent Events, gets genuine streaming today at the cost of bypassing the SDK for that call and coupling your code to one provider’s API shape.
Resources & further reading
- PHP AI Client SDK repository, GitHub, see the project’s roadmap discussion for planned streaming support
- MDN: Server-sent events, MDN
- MDN: ReadableStream, MDN