File: /home/panomity.de/vr.panomity.com/api/gpuq_client.php
<?php
/**
* GPUQ Client für CMS4VR
*
* Alle KI-Workloads (Chat, Vision-Beschreibungen, Splats, STT) laufen als
* Jobs über die GPUQ-Queue auf 127.0.0.1:8020. Konfiguration in config.gpuq.php.
*/
function gpuq_config(): array
{
static $config = null;
if ($config === null) {
$path = __DIR__ . '/config.gpuq.php';
$config = is_file($path) ? (require $path) : [];
if (!is_array($config)) {
$config = [];
}
}
return $config;
}
function gpuq_http_json(string $method, string $url, ?array $body = null, int $timeout = 15): array
{
$ch = curl_init($url);
$headers = ['Accept: application/json'];
$opts = [
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => $timeout,
CURLOPT_CONNECTTIMEOUT => 5,
];
if ($body !== null) {
$headers[] = 'Content-Type: application/json';
$opts[CURLOPT_POSTFIELDS] = json_encode($body);
}
$opts[CURLOPT_HTTPHEADER] = $headers;
curl_setopt_array($ch, $opts);
$raw = curl_exec($ch);
$err = curl_error($ch);
$code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($raw === false) {
return ['ok' => false, 'http' => 0, 'error' => $err, 'data' => []];
}
$decoded = json_decode($raw, true);
return [
'ok' => $code >= 200 && $code < 400,
'http' => $code,
'error' => null,
'data' => is_array($decoded) ? $decoded : ['raw' => $raw],
];
}
/**
* Job in die GPUQ-Queue einreihen.
*
* @return string|null job_id oder null bei Fehler
*/
function gpuq_submit(string $jobType, string $executeUrl, array $payload, string $priority = 'normal', int $timeoutSeconds = 600): ?string
{
$cfg = gpuq_config();
$base = rtrim($cfg['gpuq_base'] ?? 'http://127.0.0.1:8020', '/');
$res = gpuq_http_json('POST', $base . '/api/v1/jobs', [
'app_name' => 'cms4vr',
'job_type' => $jobType,
'priority' => $priority,
'payload' => $payload,
'execute_url' => $executeUrl,
'execute_method' => 'POST',
'timeout_seconds' => $timeoutSeconds,
]);
if (!$res['ok'] || empty($res['data']['job_id'])) {
return null;
}
return (string)$res['data']['job_id'];
}
/** Aktuellen Job-Status abrufen (oder null). */
function gpuq_job(string $jobId): ?array
{
$cfg = gpuq_config();
$base = rtrim($cfg['gpuq_base'] ?? 'http://127.0.0.1:8020', '/');
$res = gpuq_http_json('GET', $base . '/api/v1/jobs/' . rawurlencode($jobId));
return $res['ok'] ? $res['data'] : null;
}
/**
* Auf Jobende warten. $onTick wird bei jedem Poll mit dem Status-Array gerufen.
*
* @return array Letzter Job-Status (status: completed|failed|canceled|killed|timeout)
*/
function gpuq_wait(string $jobId, int $maxSeconds = 300, float $pollInterval = 1.0, ?callable $onTick = null): array
{
$deadline = microtime(true) + $maxSeconds;
$last = ['status' => 'unknown'];
while (microtime(true) < $deadline) {
$job = gpuq_job($jobId);
if ($job !== null) {
$last = $job;
if ($onTick !== null) {
$onTick($job);
}
$status = (string)($job['status'] ?? '');
if (in_array($status, ['completed', 'failed', 'canceled', 'killed'], true)) {
return $job;
}
}
usleep((int)($pollInterval * 1000000));
}
$last['status'] = $last['status'] === 'unknown' ? 'timeout' : (string)$last['status'];
$last['gpuq_wait_timeout'] = true;
return $last;
}
/**
* Chat-Komfortfunktion: messages → GPUQ vr_chat Job → Antworttext.
*
* @return array{success:bool,content:string,error?:string,job_id?:string}
*/
function gpuq_chat(array $messages, array $options = []): array
{
$cfg = gpuq_config();
$request = array_merge([
'model' => $options['model'] ?? ($cfg['chat_model'] ?? 'gpt-oss:20b'),
'messages' => $messages,
'stream' => false,
], $options['request'] ?? []);
$payload = [
'_gpuq_secret' => (string)($cfg['chat_exec_secret'] ?? ''),
'timeout_seconds' => (int)($options['timeout'] ?? 300),
'request' => $request,
];
$jobId = gpuq_submit('vr_chat', (string)($cfg['chat_exec_url'] ?? ''), $payload, $options['priority'] ?? 'high', (int)($options['timeout'] ?? 300));
if ($jobId === null) {
return ['success' => false, 'content' => '', 'error' => 'GPUQ submit failed'];
}
$job = gpuq_wait($jobId, (int)($options['max_wait'] ?? 300), (float)($options['poll'] ?? 1.0), $options['on_tick'] ?? null);
if (($job['status'] ?? '') !== 'completed') {
return ['success' => false, 'content' => '', 'error' => 'GPUQ job ' . ($job['status'] ?? 'unknown') . ': ' . (string)($job['message'] ?? ''), 'job_id' => $jobId];
}
$content = (string)($job['result']['upstream']['message']['content'] ?? '');
if ($content === '') {
return ['success' => false, 'content' => '', 'error' => 'Empty model response', 'job_id' => $jobId];
}
return ['success' => true, 'content' => $content, 'job_id' => $jobId];
}