This commit is contained in:
@@ -82,6 +82,23 @@ public sealed class AiPlanningServiceTests
|
||||
Assert.Equal("DIE ORIGINALANTWORT", error.RawResponse);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RequestPlanAsync_Zeitueberschreitung_LiefertKonkreteProxyMeldung()
|
||||
{
|
||||
var http = new HttpClient(new CanceledResponseHandler())
|
||||
{
|
||||
BaseAddress = new Uri("https://example.invalid/"),
|
||||
};
|
||||
var service = new AiPlanningService(http, new FakeLessons(), new FakeGroups([]), new FakeSubjects([]),
|
||||
new FakeCompetencyDomains(), new FakeAlternativeLessonPaths([]));
|
||||
|
||||
var error = await Assert.ThrowsAsync<AiBackendException>(() =>
|
||||
service.RequestPlanAsync(new Unit(), "", "token"));
|
||||
|
||||
Assert.Contains("3½ Minuten", error.Message);
|
||||
Assert.Contains("Webserver-Proxys", error.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Rettungsdialog_ImportiertMarkierteStundeAlsNeu()
|
||||
{
|
||||
@@ -118,6 +135,13 @@ public sealed class AiPlanningServiceTests
|
||||
});
|
||||
}
|
||||
|
||||
private sealed class CanceledResponseHandler : HttpMessageHandler
|
||||
{
|
||||
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
|
||||
CancellationToken cancellationToken) => Task.FromCanceled<HttpResponseMessage>(
|
||||
new CancellationToken(canceled: true));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildContext_FuelltGruppenUndFachKontext()
|
||||
{
|
||||
|
||||
@@ -203,7 +203,13 @@ public static class AppBootstrapper
|
||||
|
||||
// ── KI-Unterstützung (4.5.9, optional – nur wenn in den Einstellungen aktiviert) ──────
|
||||
services.AddSingleton(_ => new AiSettingsService(appData));
|
||||
services.AddSingleton(_ => new HttpClient { BaseAddress = new Uri(AiBackendUrl) });
|
||||
services.AddSingleton(_ => new HttpClient
|
||||
{
|
||||
BaseAddress = new Uri(AiBackendUrl),
|
||||
// Das PHP-Backend wartet höchstens 180 s auf umfangreiche KI-Antworten. Der Client
|
||||
// bleibt bewusst etwas länger offen, damit dessen konkrete Fehlermeldung noch ankommt.
|
||||
Timeout = TimeSpan.FromSeconds(210),
|
||||
});
|
||||
services.AddSingleton<AiPlanningService>();
|
||||
|
||||
// ── WebUntis-iCal-Abgleich (optional – nur wenn URL hinterlegt und aktiviert) ─────────
|
||||
|
||||
@@ -350,6 +350,12 @@ public class AiPlanningService(HttpClient http, ILessonRepository lessons,
|
||||
|
||||
HttpResponseMessage resp;
|
||||
try { resp = await http.SendAsync(req); }
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
throw new AiBackendException(
|
||||
"Die KI-Anfrage hat länger als 3½ Minuten gedauert und wurde beendet. " +
|
||||
"Falls das wiederholt passiert, bitte das Zeitlimit des Webserver-Proxys prüfen.");
|
||||
}
|
||||
catch (HttpRequestException)
|
||||
{
|
||||
throw new AiBackendException("Der KI-Dienst ist nicht erreichbar. Bitte Internetverbindung prüfen.");
|
||||
|
||||
@@ -188,6 +188,19 @@ deaktiviert (`AllowOverride None`), oder es läuft nginx statt Apache (dort gilt
|
||||
grundsätzlich nicht) — dann hilft nur eine serverseitige Konfiguration durch den Hoster/Support
|
||||
(z.B. bei nginx ein `fastcgi_param HTTP_AUTHORIZATION $http_authorization;`).
|
||||
|
||||
## Fehlerbehebung: KI-Antwort endet mitten im JSON
|
||||
|
||||
Eine im Rettungsdialog am Ende abgeschnittene Modellantwort entsteht typischerweise nicht durch
|
||||
die Socketverbindung, sondern durch Anthropics `stop_reason: max_tokens`. Das Backend erkennt
|
||||
diesen Fall ausdrücklich und nennt das konfigurierte Limit in der Fehlermeldung. Der empfohlene
|
||||
Wert für `max_output_tokens` ist **16000**; abgerechnet werden trotzdem nur tatsächlich erzeugte
|
||||
Tokens.
|
||||
|
||||
Für echte Zeitüberschreitungen wartet der Anthropic-cURL-Aufruf höchstens 180 Sekunden und der
|
||||
Desktop-Client 210 Sekunden. Bricht eine Anfrage reproduzierbar früher ab, liegt davor noch ein
|
||||
Zeitlimit des Hosters (nginx/Apache/FastCGI/Reverse Proxy), das auf mindestens 180 Sekunden gesetzt
|
||||
werden muss.
|
||||
|
||||
## Was hiermit NICHT geprüft ist
|
||||
|
||||
- Ob Anthropic zuverlässig valides JSON im erwarteten Schema liefert (reine Prompt-Qualitätsfrage,
|
||||
|
||||
@@ -27,7 +27,10 @@ return [
|
||||
],
|
||||
|
||||
// Sicherheitsnetz gegen ausufernde Antworten (und damit Kosten) pro Anfrage.
|
||||
'max_output_tokens' => 8000,
|
||||
// 8.000 waren bei ausführlichen Einheitenplanungen nachweislich zu knapp und führten zu
|
||||
// mitten im JSON abgeschnittenen Antworten. Das ist eine Obergrenze, kein vorab berechneter
|
||||
// Festpreis: kürzere Antworten verbrauchen und kosten weiterhin nur ihre tatsächlichen Tokens.
|
||||
'max_output_tokens' => 16000,
|
||||
|
||||
// USD je 1 Million Token, getrennt nach Input/Output/Prompt-Cache. Vor dem produktiven Einsatz
|
||||
// gegen die aktuelle Anthropic-Preisseite gegenprüfen — Preise ändern sich.
|
||||
|
||||
@@ -161,6 +161,15 @@ $result = ai_backend_call_and_charge($pdo, $config, $user, $systemPrompt, $userC
|
||||
|
||||
// Erst NACH der Abrechnung validieren: die Token wurden real verbraucht, das wird auch dann
|
||||
// verrechnet, wenn die KI kein valides JSON geliefert hat (siehe Planungsdokument).
|
||||
if (($result['stopReason'] ?? null) === 'max_tokens') {
|
||||
ai_backend_fail(502,
|
||||
'Die KI-Antwort wurde am Ausgabelimit von ' . $config['max_output_tokens'] .
|
||||
' Tokens abgeschnitten. Bitte den Umfang der Anfrage verkleinern oder das Serverlimit erhöhen.', [
|
||||
'errorCode' => 'output_limit_reached',
|
||||
'rawResponse' => $result['content'],
|
||||
]);
|
||||
}
|
||||
|
||||
$parsed = ai_backend_decode_json_response($result['content']);
|
||||
if (!is_array($parsed) || !isset($parsed['lessons']) || !is_array($parsed['lessons'])) {
|
||||
// Die bezahlte Modellantwort nicht wegwerfen: Der Desktop-Client kann sie in einem
|
||||
|
||||
@@ -37,7 +37,11 @@ class AnthropicProvider implements ProviderInterface
|
||||
],
|
||||
'messages' => [['role' => 'user', 'content' => $userContent]],
|
||||
]),
|
||||
CURLOPT_TIMEOUT => 90,
|
||||
// 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);
|
||||
@@ -68,6 +72,9 @@ class AnthropicProvider implements ProviderInterface
|
||||
|
||||
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),
|
||||
|
||||
Reference in New Issue
Block a user