258 lines
8.2 KiB
PHP
258 lines
8.2 KiB
PHP
#!/usr/bin/env php
|
|
<?php
|
|
// PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
|
|
// Copyright 2025 TimeHexOn & foxhop & russell@unturf
|
|
// https://www.permacomputer.com
|
|
|
|
/**
|
|
* UncloseAI - PHP client for OpenAI-compatible APIs with streaming support
|
|
*/
|
|
|
|
class UncloseAI {
|
|
private $models = [];
|
|
private $tts_endpoints = [];
|
|
private $api_key;
|
|
private $timeout;
|
|
private $debug;
|
|
|
|
public function __construct($config = []) {
|
|
$this->api_key = $config['api_key'] ?? null;
|
|
$this->timeout = $config['timeout'] ?? 30;
|
|
$this->debug = $config['debug'] ?? false;
|
|
|
|
$endpoints = $config['endpoints'] ?? $this->discoverEndpointsFromEnv('MODEL_ENDPOINT');
|
|
$ttsEndpoints = $config['tts_endpoints'] ?? $this->discoverEndpointsFromEnv('TTS_ENDPOINT');
|
|
|
|
if ($this->debug) {
|
|
echo "[DEBUG] Initialized with " . count($endpoints) . " endpoint(s)\n";
|
|
}
|
|
|
|
$this->discoverModels($endpoints);
|
|
$this->tts_endpoints = $ttsEndpoints;
|
|
}
|
|
|
|
public function listModels() {
|
|
return $this->models;
|
|
}
|
|
|
|
public function chat($messages, $options = []) {
|
|
$model = $options['model'] ?? null;
|
|
$modelInfo = $this->resolveModel($model);
|
|
|
|
$payload = [
|
|
'model' => $modelInfo['id'],
|
|
'messages' => $messages,
|
|
'max_tokens' => $options['max_tokens'] ?? 100,
|
|
'temperature' => $options['temperature'] ?? 0.7
|
|
];
|
|
|
|
$response = $this->httpRequest($modelInfo['endpoint'] . '/chat/completions', 'POST', $payload);
|
|
return json_decode($response, true);
|
|
}
|
|
|
|
public function chatStream($messages, $options, $callback) {
|
|
$model = $options['model'] ?? null;
|
|
$modelInfo = $this->resolveModel($model);
|
|
|
|
$payload = [
|
|
'model' => $modelInfo['id'],
|
|
'messages' => $messages,
|
|
'max_tokens' => $options['max_tokens'] ?? 500,
|
|
'temperature' => $options['temperature'] ?? 0.7,
|
|
'stream' => true
|
|
];
|
|
|
|
$url = $modelInfo['endpoint'] . '/chat/completions';
|
|
$ch = curl_init($url);
|
|
curl_setopt($ch, CURLOPT_POST, true);
|
|
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
|
|
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
|
|
curl_setopt($ch, CURLOPT_TIMEOUT, $this->timeout);
|
|
|
|
$buffer = '';
|
|
curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($curl, $data) use ($callback, &$buffer) {
|
|
$buffer .= $data;
|
|
$lines = explode("\n", $buffer);
|
|
$buffer = array_pop($lines);
|
|
|
|
foreach ($lines as $line) {
|
|
if (strpos($line, 'data: ') === 0) {
|
|
$json = substr($line, 6);
|
|
if (trim($json) === '[DONE]') {
|
|
return strlen($data);
|
|
}
|
|
$chunk = json_decode($json, true);
|
|
if ($chunk) {
|
|
call_user_func($callback, $chunk);
|
|
}
|
|
}
|
|
}
|
|
return strlen($data);
|
|
});
|
|
|
|
curl_exec($ch);
|
|
if (curl_errno($ch)) {
|
|
throw new Exception(curl_error($ch));
|
|
}
|
|
curl_close($ch);
|
|
}
|
|
|
|
public function tts($text, $voice = 'alloy', $model = 'tts-1') {
|
|
if (empty($this->tts_endpoints)) {
|
|
throw new Exception('No TTS endpoints available');
|
|
}
|
|
|
|
$payload = [
|
|
'model' => $model,
|
|
'voice' => $voice,
|
|
'input' => $text
|
|
];
|
|
|
|
return $this->httpRequest($this->tts_endpoints[0] . '/audio/speech', 'POST', $payload);
|
|
}
|
|
|
|
private function discoverEndpointsFromEnv($prefix) {
|
|
$endpoints = [];
|
|
for ($i = 1; $i < 10000; $i++) {
|
|
$endpoint = getenv("{$prefix}_{$i}");
|
|
if ($endpoint === false) break;
|
|
$endpoints[] = $endpoint;
|
|
}
|
|
return $endpoints;
|
|
}
|
|
|
|
private function discoverModels($endpoints) {
|
|
foreach ($endpoints as $endpoint) {
|
|
if ($this->debug) {
|
|
echo "[DEBUG] Discovering from: $endpoint\n";
|
|
}
|
|
|
|
try {
|
|
$response = $this->httpRequest("$endpoint/models", 'GET');
|
|
$data = json_decode($response, true);
|
|
|
|
foreach ($data['data'] as $model) {
|
|
$maxTokens = $model['max_model_len'] ?? 8192;
|
|
$this->models[] = [
|
|
'id' => $model['id'],
|
|
'endpoint' => $endpoint,
|
|
'max_tokens' => $maxTokens
|
|
];
|
|
|
|
if ($this->debug) {
|
|
echo "[DEBUG] Discovered: {$model['id']}\n";
|
|
}
|
|
}
|
|
} catch (Exception $e) {
|
|
if ($this->debug) {
|
|
echo "[DEBUG] Error: {$e->getMessage()}\n";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private function resolveModel($model) {
|
|
if (empty($this->models)) {
|
|
throw new Exception('No models available');
|
|
}
|
|
|
|
if ($model === null) {
|
|
return $this->models[0];
|
|
}
|
|
|
|
foreach ($this->models as $m) {
|
|
if ($m['id'] === $model) {
|
|
return $m;
|
|
}
|
|
}
|
|
|
|
throw new Exception("Model '$model' not found");
|
|
}
|
|
|
|
private function httpRequest($url, $method, $payload = null) {
|
|
$ch = curl_init($url);
|
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
|
curl_setopt($ch, CURLOPT_TIMEOUT, $this->timeout);
|
|
|
|
if ($method === 'POST' && $payload) {
|
|
curl_setopt($ch, CURLOPT_POST, true);
|
|
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
|
|
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
|
|
}
|
|
|
|
if ($this->api_key) {
|
|
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
|
'Content-Type: application/json',
|
|
"Authorization: Bearer {$this->api_key}"
|
|
]);
|
|
}
|
|
|
|
$response = curl_exec($ch);
|
|
|
|
if (curl_errno($ch)) {
|
|
throw new Exception(curl_error($ch));
|
|
}
|
|
|
|
curl_close($ch);
|
|
return $response;
|
|
}
|
|
}
|
|
|
|
// Demo when run as script
|
|
if (basename(__FILE__) === basename($_SERVER['PHP_SELF'])) {
|
|
echo "=== UncloseAI PHP Client (with Streaming) ===\n\n";
|
|
|
|
$client = new UncloseAI(['debug' => true]);
|
|
|
|
if (empty($client->listModels())) {
|
|
echo "ERROR: No models discovered. Set environment variables:\n";
|
|
echo " MODEL_ENDPOINT_1, MODEL_ENDPOINT_2, etc.\n";
|
|
exit(1);
|
|
}
|
|
|
|
$models = $client->listModels();
|
|
echo "\nDiscovered " . count($models) . " model(s):\n";
|
|
foreach ($models as $m) {
|
|
echo " - {$m['id']} (max_tokens: {$m['max_tokens']})\n";
|
|
}
|
|
echo "\n";
|
|
|
|
// Non-streaming chat
|
|
echo "=== Non-Streaming Chat ===\n";
|
|
$response = $client->chat([
|
|
['role' => 'system', 'content' => 'You are a helpful AI assistant.'],
|
|
['role' => 'user', 'content' => 'Explain quantum computing in one sentence.']
|
|
]);
|
|
echo "Response: " . $response['choices'][0]['message']['content'] . "\n\n";
|
|
|
|
// Streaming chat
|
|
echo "=== Streaming Chat ===\n";
|
|
$modelId = count($models) > 1 ? $models[1]['id'] : null;
|
|
echo "Model: " . ($modelId ?? $models[0]['id']) . "\n";
|
|
echo "Response: ";
|
|
|
|
$client->chatStream([
|
|
['role' => 'system', 'content' => 'You are a coding assistant.'],
|
|
['role' => 'user', 'content' => 'Write a PHP function to check if a number is prime']
|
|
], ['model' => $modelId, 'max_tokens' => 200], function($chunk) {
|
|
if (isset($chunk['choices'][0]['delta']['content'])) {
|
|
echo $chunk['choices'][0]['delta']['content'];
|
|
}
|
|
});
|
|
|
|
echo "\n\n";
|
|
|
|
// TTS
|
|
if (!empty($client->listModels())) {
|
|
echo "=== TTS Speech Generation ===\n";
|
|
try {
|
|
$audioData = $client->tts('Hello from UncloseAI PHP client! This demonstrates streaming support.');
|
|
file_put_contents('speech.mp3', $audioData);
|
|
echo "[OK] Speech file created: speech.mp3 (" . strlen($audioData) . " bytes)\n\n";
|
|
} catch (Exception $e) {
|
|
echo "[ERROR] TTS Error: " . $e->getMessage() . "\n\n";
|
|
}
|
|
}
|
|
|
|
echo "=== Examples Complete ===\n";
|
|
}
|