detect(); } public function isAvailable(): bool { return $this->mode !== null; } public function isVisionAvailable(): bool { return $this->mode === 'ollama'; } public function getProviderLabel(): ?string { return match($this->mode) { 'ollama' => 'Ollama (' . $this->url . ')', 'openai' => 'Remote-API (' . $this->url . ')', default => null, }; } public function generate(string $prompt): string { return match($this->mode) { 'ollama' => $this->callOllama($prompt), 'openai' => $this->callOpenAi($prompt), default => '', }; } /** * Sendet Bilder (base64-kodiert) zusammen mit einem Prompt an das Vision-Modell. * Nur im Ollama-Modus verfügbar. Gibt '' zurück wenn Vision nicht verfügbar. * * @param string $prompt * @param string[] $base64Images Array von base64-kodierten Bildern (JPEG/PNG) */ public function generateVision(string $prompt, array $base64Images): string { if ($this->mode !== 'ollama' || empty($base64Images)) return ''; $payload = [ 'model' => $this->visionModel, 'messages' => [[ 'role' => 'user', 'content' => $prompt, 'images' => $base64Images, ]], 'stream' => false, 'options' => ['temperature' => 0.1], ]; $ch = curl_init($this->url . '/api/chat'); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 300, CURLOPT_HTTPHEADER => ['Content-Type: application/json'], CURLOPT_POSTFIELDS => json_encode($payload), ]); $response = curl_exec($ch); $err = curl_error($ch); curl_close($ch); if ($err || !$response) return ''; $data = json_decode($response, true); return $data['message']['content'] ?? ''; } /** * Prüft ohne Timeout-Kosten, ob der Provider überhaupt erreichbar ist. * Nur für Diagnosezwecke (Einstellungsseite) – nicht im normalen Request-Pfad nutzen. */ public function isReachable(): bool { if ($this->mode === null) return false; if ($this->mode === 'ollama') return $this->probeOllama($this->url); return true; // OpenAI-Modus: kein schneller Probe verfügbar } private function detect(): void { $ollamaUrl = getenv('OLLAMA_URL') ?: ($_ENV['OLLAMA_URL'] ?? ''); $apiUrl = getenv('LLM_API_URL') ?: ($_ENV['LLM_API_URL'] ?? ''); $apiKey = getenv('LLM_API_KEY') ?: ($_ENV['LLM_API_KEY'] ?? ''); if ($ollamaUrl !== '') { $this->mode = 'ollama'; $this->url = rtrim($ollamaUrl, '/'); $this->model = getenv('OLLAMA_MODEL') ?: ($_ENV['OLLAMA_MODEL'] ?? 'llama3.2'); $this->visionModel = getenv('VISION_MODEL') ?: ($_ENV['VISION_MODEL'] ?? 'qwen2.5vl:latest'); return; } if ($apiUrl !== '' && $apiKey !== '') { $this->mode = 'openai'; $this->url = rtrim($apiUrl, '/'); $this->model = getenv('LLM_MODEL') ?: ($_ENV['LLM_MODEL'] ?? 'gpt-4o-mini'); $this->apiKey = $apiKey; return; } // Auto-Discovery: lokales Ollama auf Port 11434 if ($this->probeOllama('http://localhost:11434')) { $this->mode = 'ollama'; $this->url = 'http://localhost:11434'; $this->model = 'llama3.2'; } } private function probeOllama(string $baseUrl): bool { $ch = curl_init($baseUrl . '/api/tags'); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 2, CURLOPT_CONNECTTIMEOUT => 2, CURLOPT_NOBODY => false, ]); curl_exec($ch); $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); $err = curl_errno($ch); curl_close($ch); return $err === 0 && $code >= 200 && $code < 300; } private function callOllama(string $prompt): string { $payload = [ 'model' => $this->model, 'prompt' => $prompt, 'stream' => false, ]; $endpoint = $this->url . '/api/generate'; $ch = curl_init($endpoint); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 300, CURLOPT_HTTPHEADER => ['Content-Type: application/json'], CURLOPT_POSTFIELDS => json_encode($payload), ]); $response = curl_exec($ch); $err = curl_error($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($err) { error_log("[Steuerkram] LlmService::callOllama() cURL-Fehler auf {$endpoint}: {$err}"); return ''; } if (!$response) { error_log("[Steuerkram] LlmService::callOllama() leere Antwort von {$endpoint} (HTTP {$httpCode})"); return ''; } return json_decode($response, true)['response'] ?? ''; } private function callOpenAi(string $prompt): string { $payload = [ 'model' => $this->model, 'messages' => [['role' => 'user', 'content' => $prompt]], ]; $ch = curl_init($this->url . '/chat/completions'); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 300, CURLOPT_HTTPHEADER => [ 'Content-Type: application/json', 'Authorization: Bearer ' . $this->apiKey, ], CURLOPT_POSTFIELDS => json_encode($payload), ]); $response = curl_exec($ch); $err = curl_error($ch); curl_close($ch); if ($err || !$response) return ''; $data = json_decode($response, true); return $data['choices'][0]['message']['content'] ?? ''; } }