KI-Feature 4.5.21: didaktischer Hintergrund je Stunde, nur auf Nachfrage
Neuer Endpunkt ai-backend/explain.php mit eigenem statischen Systemprompt (Begründung des Phasenaufbaus, mögliche Stolpersteine, Differenzierungsideen). Bewusst als separater Endpunkt statt Zusatzfeld in jeder plan.php-Antwort, damit die Erklärung nur bei tatsächlicher Nutzung abgerechnet wird statt bei jeder Planungsanfrage mitgeneriert zu werden. Die Guthaben-Abrechnung (SELECT-FOR-UPDATE, Transaktions-Insert) wurde aus plan.php nach ai_backend_call_and_charge in db.php ausgelagert, damit sie nicht an zwei Stellen gepflegt werden muss. Kein neues DB-Schema nötig. Im AiAssistDialog erscheint je Stunde ein Button "Didaktischen Hintergrund erklären", der nach dem Laden durch den Text ersetzt wird. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/providers/AnthropicProvider.php';
|
||||
require_once __DIR__ . '/providers/FakeProvider.php';
|
||||
|
||||
/** Baut eine PDO-Verbindung aus config.php auf. */
|
||||
function ai_backend_db(array $config): PDO
|
||||
{
|
||||
@@ -73,3 +76,80 @@ function ai_backend_fail(int $httpStatus, string $message): never
|
||||
echo json_encode(['error' => $message]);
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ruft das konfigurierte LLM (oder den FakeProvider für lokale Tests, siehe README.md) auf und
|
||||
* verrechnet die echten Token-Kosten gegen das Guthaben des Nutzers. Gemeinsame Logik für jeden
|
||||
* Endpunkt, der einen LLM-Call abrechnet (plan.php, explain.php) — die SELECT-FOR-UPDATE-
|
||||
* Absicherung gegen Race Conditions bei gleichzeitigen Anfragen desselben Nutzers soll nicht an
|
||||
* mehreren Stellen gepflegt werden müssen. Gibt bei Erfolg das Provider-Ergebnis unverändert
|
||||
* zurück (inkl. "content", das der Aufrufer je nach Endpunkt selbst auswertet).
|
||||
*/
|
||||
function ai_backend_call_and_charge(PDO $pdo, array $config, array $user, string $systemPrompt, string $userContent): array
|
||||
{
|
||||
$useFake = getenv('AI_BACKEND_FAKE_PROVIDER') === '1';
|
||||
if ($useFake) {
|
||||
$provider = new FakeProvider();
|
||||
$modelKey = 'fake';
|
||||
} else {
|
||||
$providerName = $config['llm_provider'];
|
||||
if ($providerName !== 'anthropic') {
|
||||
ai_backend_fail(500, "Provider '$providerName' ist nicht implementiert.");
|
||||
}
|
||||
$modelKey = $config['anthropic']['model'];
|
||||
$provider = new AnthropicProvider($config['anthropic']['api_key'], $modelKey);
|
||||
}
|
||||
|
||||
try {
|
||||
$result = $provider->sendMessage($systemPrompt, $userContent, $config['max_output_tokens']);
|
||||
} catch (RuntimeException $e) {
|
||||
ai_backend_fail(502, $e->getMessage());
|
||||
}
|
||||
|
||||
$defaultPricing = ['input' => 0, 'output' => 0, 'cache_write' => 0, 'cache_read' => 0];
|
||||
$pricing = $config['pricing'][$modelKey] ?? ($useFake ? $defaultPricing : null);
|
||||
if ($pricing === null) {
|
||||
ai_backend_fail(500, "Kein Preis für Modell '$modelKey' konfiguriert.");
|
||||
}
|
||||
$pricing += $defaultPricing; // fehlende cache_write/cache_read in älteren config.php-Einträgen -> 0
|
||||
|
||||
$cacheCreationTokens = $result['cacheCreationInputTokens'] ?? 0;
|
||||
$cacheReadTokens = $result['cacheReadInputTokens'] ?? 0;
|
||||
$cost = ($result['inputTokens'] / 1_000_000 * $pricing['input'])
|
||||
+ ($result['outputTokens'] / 1_000_000 * $pricing['output'])
|
||||
+ ($cacheCreationTokens / 1_000_000 * $pricing['cache_write'])
|
||||
+ ($cacheReadTokens / 1_000_000 * $pricing['cache_read']);
|
||||
|
||||
// Guthaben abziehen und Transaktion protokollieren — mit Zeilensperre, damit zwei gleichzeitige
|
||||
// Anfragen desselben Nutzers das Guthaben nicht versehentlich unter 0 drücken können.
|
||||
$pdo->beginTransaction();
|
||||
try {
|
||||
$stmt = $pdo->prepare('SELECT balance_usd FROM users WHERE id = ? FOR UPDATE');
|
||||
$stmt->execute([$user['id']]);
|
||||
$currentBalance = (float) $stmt->fetchColumn();
|
||||
|
||||
if ($currentBalance - $cost < 0) {
|
||||
$pdo->rollBack();
|
||||
ai_backend_fail(402, 'Guthaben würde durch diese Anfrage negativ werden.');
|
||||
}
|
||||
|
||||
$newBalance = $currentBalance - $cost;
|
||||
$pdo->prepare('UPDATE users SET balance_usd = ? WHERE id = ?')->execute([$newBalance, $user['id']]);
|
||||
$pdo->prepare(
|
||||
'INSERT INTO transactions
|
||||
(user_id, type, model, input_tokens, output_tokens,
|
||||
cache_creation_input_tokens, cache_read_input_tokens, cost_usd, balance_after)
|
||||
VALUES (?, "usage", ?, ?, ?, ?, ?, ?, ?)'
|
||||
)->execute([
|
||||
$user['id'], $modelKey, $result['inputTokens'], $result['outputTokens'],
|
||||
$cacheCreationTokens, $cacheReadTokens, $cost, $newBalance,
|
||||
]);
|
||||
|
||||
$pdo->commit();
|
||||
} catch (Throwable $e) {
|
||||
$pdo->rollBack();
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user