$messages * @return string|null the assistant message content, or null on any failure */ public function chat(array $messages, array $options = []): ?string { if (! $this->enabled()) { return null; } try { $response = $this->client()->post('/chat/completions', [ 'model' => Settings::get('ai_model'), 'messages' => $messages, ...$options, ]); if (! $response->successful()) { return null; } return $response->json('choices.0.message.content'); } catch (\Throwable) { return null; } } /** * Tests unsaved admin-form values directly, rather than whatever's * currently stored — mirrors BookStackClient::testConnection(). * * @return array{ok: bool, message: ?string} */ public function testConnection(string $baseUrl, string $apiKey, string $model, bool $verifySsl = true): array { try { $http = Http::withOptions(['verify' => $verifySsl]) ->timeout(10) ->baseUrl(rtrim($baseUrl, '/')); if ($apiKey !== '') { $http = $http->withToken($apiKey); } $response = $http->post('/chat/completions', [ 'model' => $model, 'messages' => [['role' => 'user', 'content' => 'ping']], 'max_tokens' => 1, ]); if ($response->successful()) { return ['ok' => true, 'message' => null]; } return ['ok' => false, 'message' => $response->json('error.message') ?? ('HTTP '.$response->status())]; } catch (\Throwable $e) { return ['ok' => false, 'message' => $e->getMessage()]; } } protected function client() { $http = Http::withOptions(['verify' => Settings::bool('ai_verify_ssl')]) ->timeout(60) ->baseUrl(rtrim(Settings::get('ai_base_url'), '/')); $apiKey = Settings::get('ai_api_key'); return $apiKey ? $http->withToken($apiKey) : $http; } }