'user', 'content' => '...']] * @param array $options Override defaults (model, max_tokens, temperature, etc.) * @return array ['success' => bool, 'content' => string, 'usage' => array, 'error' => string] */ abstract public function sendPrompt(array $messages, array $options = []): array; /** * Stream a prompt response chunk by chunk. * * @param array $messages * @param array $options * @param callable $callback Called with each chunk: function(string $chunk) * @return array ['success' => bool, 'error' => string] */ abstract public function streamPrompt(array $messages, array $options, callable $callback): array; /** * Return available models for this provider. * * @return array [['id' => '...', 'label' => '...']] */ abstract public function getModels(): array; /** * Test the API key is valid and the provider is reachable. * * @return array ['success' => bool, 'latency_ms' => int, 'error' => string] */ abstract public function testConnection(): array; // ─── Fluent Setters ────────────────────────────────────────────────────── public function getModel(): string { return $this->model; } public function setModel(string $model): static { $this->model = $model; return $this; } public function setMaxTokens(int $tokens): static { $this->maxTokens = $tokens; return $this; } public function setSystemPrompt(string $prompt): static { $this->systemPrompt = $prompt; return $this; } public function setTemperature(float $temp): static { $this->temperature = $temp; return $this; } public function setTimeout(int $seconds): static { $this->timeout = $seconds; return $this; } // ─── Shared Helpers ────────────────────────────────────────────────────── /** * Build a standard error response. */ protected function errorResponse(string $message): array { return ['success' => false, 'content' => '', 'usage' => [], 'error' => $message]; } /** * Build a standard success response. */ protected function successResponse(string $content, array $usage = []): array { return ['success' => true, 'content' => $content, 'usage' => $usage, 'error' => '']; } /** * Execute a cURL request and return the decoded JSON response. */ protected function curlPost(string $url, array $headers, array $payload): array { $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode($payload), CURLOPT_HTTPHEADER => $headers, CURLOPT_TIMEOUT => $this->timeout, ]); $body = curl_exec($ch); $error = curl_error($ch); $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($error) { throw new \Exception("cURL error: $error"); } $decoded = json_decode($body, true); if ($code >= 400) { $msg = $decoded['error']['message'] ?? $decoded['message'] ?? "HTTP $code error"; throw new \Exception($msg); } return $decoded; } /** * Execute a streaming cURL request, calling $callback for each SSE data chunk. */ protected function curlStream(string $url, array $headers, array $payload, callable $callback): void { $payload['stream'] = true; $errorBody = ''; $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => false, CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode($payload), CURLOPT_HTTPHEADER => $headers, CURLOPT_TIMEOUT => $this->timeout, CURLOPT_WRITEFUNCTION => function($ch, $data) use ($callback, &$errorBody) { $lines = explode("\n", $data); $hasSseData = false; foreach ($lines as $line) { $line = trim($line); if (str_starts_with($line, 'data: ')) { $hasSseData = true; $json = substr($line, 6); if ($json === '[DONE]') break; $chunk = json_decode($json, true); if ($chunk) $callback($chunk); } } // If no SSE data lines were found this chunk may be an error body if (!$hasSseData) { $errorBody .= $data; } return strlen($data); }, ]); curl_exec($ch); $curlError = curl_error($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($curlError) { throw new \Exception("cURL stream error: $curlError"); } if ($httpCode >= 400) { $decoded = json_decode(trim($errorBody), true); $msg = $decoded['error']['message'] ?? $decoded['message'] ?? "API error (HTTP $httpCode)"; throw new \Exception($msg); } } }