config = $config ?? config('LLMapi.default', []); $baseUrl = rtrim((string) ($this->config['base_url'] ?? ''), '/'); $this->client = new Client([ 'base_uri' => $baseUrl . '/', 'timeout' => (int) ($this->config['timeout'] ?? 60), 'connect_timeout' => (int) ($this->config['connect_timeout'] ?? 10), ]); } public function isConfigured(): bool { return trim((string) ($this->config['api_key'] ?? '')) !== '' && trim((string) ($this->config['base_url'] ?? '')) !== ''; } public function chatJson(array $messages, array $options = []): array { $content = $this->chat($messages, $options); $decoded = json_decode($this->extractJson($content), true); if (!is_array($decoded)) { throw new RuntimeException('LLM response is not valid JSON.'); } return $decoded; } public function chat(array $messages, array $options = []): string { if (!$this->isConfigured()) { throw new RuntimeException('LLM API is not configured.'); } $headers = [ 'Authorization' => 'Bearer ' . $this->config['api_key'], 'Content-Type' => 'application/json', ]; if (!empty($this->config['organization'])) { $headers['OpenAI-Organization'] = $this->config['organization']; } if (!empty($this->config['project'])) { $headers['OpenAI-Project'] = $this->config['project']; } $body = [ 'model' => $options['model'] ?? config('LLMapi.chat.model'), 'messages' => $messages, 'temperature' => $options['temperature'] ?? config('LLMapi.chat.temperature', 0.2), 'max_tokens' => $options['max_tokens'] ?? config('LLMapi.chat.max_tokens', 1200), 'stream' => (bool) ($options['stream'] ?? config('LLMapi.chat.stream', false)), ]; if (array_key_exists('response_format', $options) && is_array($options['response_format'])) { $body['response_format'] = $options['response_format']; } if (array_key_exists('thinking', $options) && is_array($options['thinking'])) { $body['thinking'] = $options['thinking']; } if (array_key_exists('request_id', $options) && is_string($options['request_id'])) { $body['request_id'] = $options['request_id']; } if (array_key_exists('user_id', $options) && is_string($options['user_id'])) { $body['user_id'] = $options['user_id']; } try { $response = $this->client->post('chat/completions', [ 'headers' => $headers, 'json' => $body, ]); } catch (RequestException $exception) { throw $this->requestException($exception); } catch (Throwable $exception) { throw new RuntimeException('LLM chat request failed: ' . $exception->getMessage(), 0, $exception); } $payload = json_decode((string) $response->getBody(), true); $content = $payload['choices'][0]['message']['content'] ?? null; if (!is_string($content) || trim($content) === '') { throw new RuntimeException('LLM chat response is empty.'); } return $content; } private function extractJson(string $content): string { $content = trim($content); $content = preg_replace('/^```(?:json)?\s*/i', '', $content) ?? $content; $content = preg_replace('/\s*```$/', '', $content) ?? $content; $start = strpos($content, '{'); $end = strrpos($content, '}'); if ($start !== false && $end !== false && $end > $start) { return substr($content, $start, $end - $start + 1); } return $content; } private function requestException(RequestException $exception): LLMRequestException { $statusCode = $exception->getResponse()?->getStatusCode(); $body = $exception->getResponse() ? (string) $exception->getResponse()->getBody() : ''; $payload = json_decode($body, true); $providerCode = null; $providerMessage = null; if (is_array($payload)) { $providerCode = isset($payload['error']['code']) ? (string) $payload['error']['code'] : null; $providerMessage = isset($payload['error']['message']) ? (string) $payload['error']['message'] : null; } $message = 'LLM chat request failed'; if ($statusCode !== null) { $message .= " with HTTP {$statusCode}"; } if ($providerCode !== null) { $message .= " and provider code {$providerCode}"; } if ($providerMessage !== null) { $message .= ": {$providerMessage}"; } else { $message .= ': ' . $exception->getMessage(); } return new LLMRequestException($message, $statusCode, $providerCode, is_array($payload) ? $payload : null); } }