preparer->prepare($absolutePath); $response = Http::withToken($apiKey) ->connectTimeout(min(5, max(1, $timeout))) ->timeout($timeout) ->retry( [200, 500], when: static fn (Throwable $exception): bool => $exception instanceof ConnectionException || ($exception instanceof RequestException && in_array($exception->response->status(), [429, 500, 502, 503, 504], true)), throw: false, ) ->acceptJson() ->post($config['url'] ?? 'https://api.openai.com/v1/chat/completions', [ 'model' => $model, 'response_format' => ['type' => 'json_object'], 'messages' => [ ['role' => 'system', 'content' => $this->systemPrompt()], [ 'role' => 'user', 'content' => [ ['type' => 'text', 'text' => 'Sichte dieses Bild nach den Kategorien im Schema und antworte nur mit dem JSON-Objekt.'], [ 'type' => 'image_url', 'image_url' => [ 'url' => 'data:image/jpeg;base64,'.base64_encode($prepared['bytes']), 'detail' => 'auto', ], ], ], ], ], ]); if ($response->failed()) { throw new RuntimeException("OpenAI-Bildsichtung fehlgeschlagen (HTTP {$response->status()})."); } $content = $response->json('choices.0.message.content'); if (! is_string($content) || trim($content) === '') { throw new RuntimeException('OpenAI-Bildsichtung lieferte keinen Inhalt.'); } $decoded = json_decode($content, true); if (! is_array($decoded)) { throw new RuntimeException('OpenAI-Bildsichtung lieferte kein JSON-Objekt.'); } $missing = array_filter(ImagePrecheckCategory::cases(), fn (ImagePrecheckCategory $c): bool => ! array_key_exists($c->value, $decoded)); if (count($missing) > 0) { throw new RuntimeException('OpenAI-Bildsichtung lieferte ein unvollständiges Schema.'); } return ImagePrecheckResult::ok($decoded, 'openai', $model, null, [ 'prompt_version' => (string) config('images.precheck.prompt_version'), 'sent_size' => [$prepared['width'], $prepared['height']], 'usage' => $response->json('usage'), ]); } private function systemPrompt(): string { $keys = implode(', ', array_map(fn (ImagePrecheckCategory $c): string => $c->value, ImagePrecheckCategory::cases())); return <<