HEX
Server: LiteSpeed
System: Linux houston.panomity.com 6.8.0-100-generic #100-Ubuntu SMP PREEMPT_DYNAMIC Tue Jan 13 16:40:06 UTC 2026 x86_64
User: nudepix (1011)
PHP: 7.4.33
Disabled: pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,pcntl_unshare,
Upload Files
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];
}