Clean-room copy of the reusable engines from comiida, with all instance data, secrets, dependencies, and build output excluded: - app/ Astro theme skeleton (no comiida blog posts; hero image -> placeholder) - api/ SeedProject PHP framework (no vendor/.env/config.php) - content-pipeline/ engine only (scripts/admin/prompts; empty runtime state) - astroagent.config.json + app/.astroagent/skills Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SYHWLHihq3v9nxNwoPCKSn
191 lines
6 KiB
PHP
191 lines
6 KiB
PHP
<?php
|
|
|
|
namespace App\LLM;
|
|
|
|
/**
|
|
* Abstract base class for all LLM provider services.
|
|
* Every svc*.php must extend this and implement all abstract methods.
|
|
*/
|
|
abstract class LLMProvider
|
|
{
|
|
protected string $apiKey = '';
|
|
protected string $model = '';
|
|
protected string $systemPrompt = '';
|
|
protected int $maxTokens = 4096;
|
|
protected float $temperature = 0.7;
|
|
protected int $timeout = 120;
|
|
|
|
/**
|
|
* Send a prompt and return the full response.
|
|
*
|
|
* @param array $messages [['role' => '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);
|
|
}
|
|
}
|
|
}
|