85 lines
3.7 KiB
PHP
85 lines
3.7 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
require_once __DIR__ . '/ProviderInterface.php';
|
|
|
|
/**
|
|
* Ruft die Anthropic Messages API direkt per curl auf — bewusst ohne SDK-Abhängigkeit.
|
|
*
|
|
* Der Systemprompt wird als eigener, mit "cache_control" markierter Content-Block gesendet
|
|
* (Prompt Caching, siehe TODO 4.5.9-Nachtrag) statt als einfacher String — er ist über alle
|
|
* Anfragen hinweg identisch (siehe plan.php), also ein idealer Kandidat: wiederholte Aufrufe
|
|
* innerhalb der Cache-TTL (Anthropic-Standard: 5 Minuten) zahlen dafür nur den stark reduzierten
|
|
* "Cache-Read"-Preis statt des vollen Input-Preises. Der erste Aufruf nach TTL-Ablauf zahlt einen
|
|
* kleinen Aufpreis fürs Neuschreiben des Caches ("cache_creation_input_tokens") — siehe
|
|
* plan.php für die Abrechnung beider Fälle.
|
|
*/
|
|
class AnthropicProvider implements ProviderInterface
|
|
{
|
|
public function __construct(private string $apiKey, private string $model) {}
|
|
|
|
public function sendMessage(string $systemPrompt, string $userContent, int $maxTokens): array
|
|
{
|
|
$ch = curl_init('https://api.anthropic.com/v1/messages');
|
|
curl_setopt_array($ch, [
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_POST => true,
|
|
CURLOPT_HTTPHEADER => [
|
|
'content-type: application/json',
|
|
'x-api-key: ' . $this->apiKey,
|
|
'anthropic-version: 2023-06-01',
|
|
],
|
|
CURLOPT_POSTFIELDS => json_encode([
|
|
'model' => $this->model,
|
|
'max_tokens' => $maxTokens,
|
|
'system' => [
|
|
['type' => 'text', 'text' => $systemPrompt, 'cache_control' => ['type' => 'ephemeral']],
|
|
],
|
|
'messages' => [['role' => 'user', 'content' => $userContent]],
|
|
]),
|
|
// Umfangreiche Einheitenplanungen können bei großen Antworten länger dauern. Der
|
|
// Desktop-Client wartet etwas länger (210 s), damit immer dieser Server mit einer
|
|
// verständlichen Fehlermeldung antworten kann, statt dass zuerst der Client abbricht.
|
|
CURLOPT_CONNECTTIMEOUT => 15,
|
|
CURLOPT_TIMEOUT => 180,
|
|
]);
|
|
$raw = curl_exec($ch);
|
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
$curlError = curl_error($ch);
|
|
curl_close($ch);
|
|
|
|
if ($raw === false) {
|
|
throw new RuntimeException("Anthropic-Anfrage fehlgeschlagen: $curlError");
|
|
}
|
|
if ($httpCode >= 400) {
|
|
throw new RuntimeException("Anthropic-API-Fehler (HTTP $httpCode): " . substr((string) $raw, 0, 500));
|
|
}
|
|
|
|
$data = json_decode((string) $raw, true);
|
|
if (!is_array($data)) {
|
|
throw new RuntimeException('Anthropic-Antwort konnte nicht als JSON gelesen werden.');
|
|
}
|
|
if (($data['stop_reason'] ?? null) === 'refusal') {
|
|
throw new RuntimeException('Die KI hat die Anfrage abgelehnt.');
|
|
}
|
|
|
|
$text = '';
|
|
foreach (($data['content'] ?? []) as $block) {
|
|
if (($block['type'] ?? null) === 'text') {
|
|
$text .= $block['text'];
|
|
}
|
|
}
|
|
|
|
return [
|
|
'content' => $text,
|
|
// Insbesondere "max_tokens" darf nicht verloren gehen: Dann ist der Text zwar eine
|
|
// technisch erfolgreiche API-Antwort, das JSON aber zwangsläufig am Ende abgeschnitten.
|
|
'stopReason' => $data['stop_reason'] ?? null,
|
|
'inputTokens' => (int) ($data['usage']['input_tokens'] ?? 0),
|
|
'outputTokens' => (int) ($data['usage']['output_tokens'] ?? 0),
|
|
'cacheCreationInputTokens' => (int) ($data['usage']['cache_creation_input_tokens'] ?? 0),
|
|
'cacheReadInputTokens' => (int) ($data['usage']['cache_read_input_tokens'] ?? 0),
|
|
];
|
|
}
|
|
}
|