Assistants Streaming Success w/ REST API in PHP?

I built a custom chat assistant into my app using php & cURL (Wordpress environment). Streaming works great in the prototype of a single file. However, when I try to implement streaming using a REST API to protect the keys I am beyond my depth of expertise (which is admittedly, very shallow). Every attempt fails with streaming (cannot keep network connection open) and hosting (SG) swears everything is configured to work.

Questions:
1. PHP REST API / STREAM EXAMPLE: Have you created or seen a php rest api that feeds assistant responses, using the streaming feature, to a front end chatbox?

2. AM I CRAZY: My limited knowledge suggests this is pretty much the only way to protect / hide keys from clients. Am I crazy? Is there a better solution that is more elegantly simple?

OVERVIEW, CODE & DETAILS

sendMessage

   function sendMessage() {
        const message = $('#vlp-ai2-input').val().trim();
        if (!message) {
            console.error('No message provided.');
            return;
        }

        $('#vlp-ai2-input').val(''); // Clear the input box
        const chatOutput = $('.vlp-ai2-chat-output');
        chatOutput.append(`<p>You: ${message}</p>`);

        const assistantMessage = $('<p>').addClass('assistant-temp').text('');
        chatOutput.append(assistantMessage);

        fetch('<*REST RELATIVE URL HERE*>', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({ message }),
            custom_token: vlpAiData.custom_token, // Include the token in the request
        })
            .then((response) => {
                if (!response.ok) {
                    throw new Error(`HTTP error! status: ${response.status}`);
                }
                const reader = response.body.getReader();
                const decoder = new TextDecoder();
                let assistantText = '';

                function processChunk() {
                    reader.read().then(({ done, value }) => {
                        if (done) {
                            chatOutput.find('.assistant-temp').removeClass('assistant-temp');
                            return;
                        }
                        const chunk = decoder.decode(value, { stream: true });
                        const lines = chunk.split('\n').filter((line) => line.trim());
                        lines.forEach((line) => {
                            if (line.startsWith('data:')) {
                                const data = line.slice(5).trim();
                                if (data === '[DONE]') return;
                                try {
                                    const json = JSON.parse(data);
                                    if (
                                        json.object === 'thread.message.delta' &&
                                        Array.isArray(json.delta?.content)
                                    ) {
                                        json.delta.content.forEach((block) => {
                                            if (block.type === 'text' && block.text?.value) {
                                                assistantText += block.text.value;
                                                chatOutput.find('.assistant-temp').text(assistantText);
                                            }
                                        });
                                    }
                                } catch (e) {
                                    console.error('Error parsing chunk:', e);
                                }
                            }
                        });
                        processChunk();
                    });
                }
                processChunk();
            })
            .catch((error) => {
                console.error('Fetch error:', error);
            });
    }

PHP REST CODE

<?php
define('WP_USE_THEMES', false);
require_once($_SERVER['DOCUMENT_ROOT'] . '/wp-load.php');

// Kept getting errors on output before headers so forced a flush
while (ob_get_level()) {
    if (ob_get_level() > 0) {
        ob_flush();
    }
    flush();
}

header("Content-Type: application/json");
header("Transfer-Encoding: chunked");
header("Cache-Control: no-cache");
header("Access-Control-Allow-Origin: *");

$input = json_decode(file_get_contents('php://input'), true);

if (empty($input['message'])) {
    echo json_encode(['error' => 'Message is required']);
    exit;
}

// Prepare OpenAI API request
$api_key = '<*MY API KEY*>';
$assistant_id = '<*ASSISTANT ID*>';
$openai_request = [
    'assistant_id' => $assistant_id,
    'thread' => [
        'messages' => [
            ['role' => 'user', 'content' => $input['message']],
        ],
    ],
    'stream' => true,
];

// Make the OpenAI API call
$ch = curl_init('https://api.openai.com/v1/threads/runs');
curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer ' . $api_key,
    'Content-Type: application/json',
    'OpenAI-Beta: assistants=v2',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($openai_request));
curl_setopt($ch, CURLOPT_WRITEFUNCTION, function ($curl, $chunk) {
    return strlen($chunk);
});
curl_exec($ch);

// Error Handling
if (curl_errno($ch)) {
    echo json_encode(['error' => 'OpenAI API error: ' . curl_error($ch)]);
}
curl_close($ch);

Yes, I realize I may still be crazy, regardless of your answers, but alas… Thanks in advance, for any responses that permit me to retain some of my hair before I pull it all out…

Thanks all!

1 Like

How do you run the script?

2 Likes

Thanks for asking, Jochen. I added code snippets and a diagram to the original post above. I’ll be embarrassed if it turns out to be something obvious, but I seem unable to track it down…

1 Like

That wasn’t my question. Nginx, Apache, Caddy,…?

Also, maybe you want to get rid of the hustle by using crowd developed stuff…

1 Like

Sorry, Apache. Though it’s siteground hosted and as I understand they do some combination of Apache with Nginx.

1 Like

Seriously considering that - thanks for the reminder. My original intent was to keep it focused on only what we need and not load a lot of resources we do not use. However, recognizing my own limitations, I see the conflict in that thinking.

I appreciate people who want to understand the stuff they do though. But I would strongly advice to use that lib for production purposes.

Also for streaming you might - if you have a lot of time - look into nodejs (PHP is not really made for that - I made the comparison with the stream lib REACTPHP and nodejs and you can literally hear it on the CPU fan that PHP is not the prefered choice for this…)

Anyways here is the link for the fanboys: Stream - ReactPHP

Ah and here is somethign that I havn’t tried: Non-blocking I/O Streams in PHP | AMPHP

Maybe you want to give it a chance - or look into their code to see how it is done.

5 Likes

Really appreciate the pointers, @jochenschultz . I’m investigating and seriously considering the OpenAI PHP API client on github, especially since we’re still only really in prototype / poc phase. It’s been holding me up way too long… Then probably look at node.js for scalability later. I appreciate you.

1 Like

If I recall correctly, with Apache, you need to periodically send a ping to your WebSocket to keep the connection alive.

In a project where we used WebSockets with PHP, I had to implement a solution like this to ensure the connection remained stable:


let ws = new WebSocket(url);

ws.onopen = function (event) {

    // Keep the socket alive

    let intervalId = setInterval(function () {

        if (ws.readyState === WebSocket.OPEN) {

            ws.send("");

        } else {

            clearInterval(intervalId);

        }
    }, 10 * 1000); // Every 10 seconds
};

It might solve your issue

1 Like