fix(email): support legacy AI gateway models

This commit is contained in:
2026-09-08 10:07:13 +02:00
parent 41e4cad929
commit a60570e0eb
3 changed files with 59 additions and 15 deletions

View File

@@ -75,17 +75,14 @@ export function buildSuggestionMail(message: any) {
} }
} }
export async function suggestEmailEntities(message: any, allCandidates: EntityCandidate[]) { export function buildEntitySuggestionRequest(
const central = Boolean(secrets.FEDEO_CENTRAL_SERVICES_ENABLED && centralServicesClient.configured()) mail: ReturnType<typeof buildSuggestionMail>,
if (!central && !secrets.OPENAI_API_KEY) { candidates: EntityCandidate[],
throw Object.assign(new Error("Die KI-Erkennung ist noch nicht konfiguriert."), { statusCode: 503 }) model = "gpt-5.6-luna",
} ) {
const mail = buildSuggestionMail(message) return {
const candidates = selectCandidates(allCandidates, JSON.stringify(mail)) model,
if (!candidates.length) return [] ...(model.startsWith("gpt-5") ? { reasoning_effort: "none" } : {}),
const request: any = {
model: "gpt-5.6-luna",
reasoning_effort: "none",
store: false, store: false,
max_completion_tokens: 1500, max_completion_tokens: 1500,
response_format: zodResponseFormat(suggestionFormat as any, "email_entity_suggestions"), response_format: zodResponseFormat(suggestionFormat as any, "email_entity_suggestions"),
@@ -94,9 +91,35 @@ export async function suggestEmailEntities(message: any, allCandidates: EntityCa
{ role: "user", content: JSON.stringify({ mail, candidates }) }, { role: "user", content: JSON.stringify({ mail, candidates }) },
], ],
} }
const completion = central }
? await centralServicesClient.aiChatCompletions(request)
: await new OpenAI({ apiKey: secrets.OPENAI_API_KEY, timeout: 45000, maxRetries: 0 }).chat.completions.create(request) export function shouldRetryWithLegacyAiModel(error: any) {
return [400, 404, 422, 500, 502].includes(Number(error?.status))
}
export async function suggestEmailEntities(message: any, allCandidates: EntityCandidate[]) {
const central = Boolean(secrets.FEDEO_CENTRAL_SERVICES_ENABLED && centralServicesClient.configured())
if (!central && !secrets.OPENAI_API_KEY) {
throw Object.assign(new Error("Die KI-Erkennung ist noch nicht konfiguriert."), { statusCode: 503 })
}
const mail = buildSuggestionMail(message)
const candidates = selectCandidates(allCandidates, JSON.stringify(mail))
if (!candidates.length) return []
const request: any = buildEntitySuggestionRequest(mail, candidates)
let completion: any
if (central) {
try {
completion = await centralServicesClient.aiChatCompletions(request)
} catch (error: any) {
if (!shouldRetryWithLegacyAiModel(error)) throw error
completion = await centralServicesClient.aiChatCompletions(
buildEntitySuggestionRequest(mail, candidates, "gpt-4o-mini"),
)
}
} else {
completion = await new OpenAI({ apiKey: secrets.OPENAI_API_KEY, timeout: 45000, maxRetries: 0 })
.chat.completions.create(request)
}
const choice = completion.choices?.[0] const choice = completion.choices?.[0]
if (choice?.finish_reason !== "stop" || choice?.message?.refusal || !choice?.message?.content) { if (choice?.finish_reason !== "stop" || choice?.message?.refusal || !choice?.message?.content) {
throw new Error("Die KI konnte keine vollständige Analyse liefern. Bitte erneut versuchen.") throw new Error("Die KI konnte keine vollständige Analyse liefern. Bitte erneut versuchen.")

View File

@@ -2,8 +2,10 @@ import assert from "node:assert/strict"
import test from "node:test" import test from "node:test"
import { import {
buildSuggestionMail, buildSuggestionMail,
buildEntitySuggestionRequest,
dismissEntitySuggestion, dismissEntitySuggestion,
selectCandidates, selectCandidates,
shouldRetryWithLegacyAiModel,
validateSuggestions, validateSuggestions,
type EntityCandidate, type EntityCandidate,
} from "../src/modules/email/email.entity-suggestions" } from "../src/modules/email/email.entity-suggestions"
@@ -69,3 +71,22 @@ test("dismisses only the selected entity suggestion", () => {
) )
assert.deepEqual(dismissEntitySuggestion(null, "customers", 1), []) assert.deepEqual(dismissEntitySuggestion(null, "customers", 1), [])
}) })
test("uses Luna by default and omits reasoning effort for the legacy fallback", () => {
const request = buildEntitySuggestionRequest(buildSuggestionMail({ subject: "Test" }), [customer])
assert.equal(request.model, "gpt-5.6-luna")
assert.equal(request.reasoning_effort, "none")
const fallback = buildEntitySuggestionRequest(buildSuggestionMail({ subject: "Test" }), [customer], "gpt-4o-mini")
assert.equal(fallback.model, "gpt-4o-mini")
assert.equal("reasoning_effort" in fallback, false)
})
test("retries model compatibility errors without retrying auth, rate-limit, or configuration errors", () => {
for (const status of [400, 404, 422, 500, 502]) {
assert.equal(shouldRetryWithLegacyAiModel({ status }), true)
}
for (const status of [401, 403, 429, 503]) {
assert.equal(shouldRetryWithLegacyAiModel({ status }), false)
}
})

View File

@@ -6,7 +6,7 @@ Die Analyse wird pro Mail gespeichert, auch wenn sie keine Treffer findet. Erneu
## Betrieb ## Betrieb
Vor dem Einsatz die Backend-Migrationen mit `npm run migrate` im Backend-Verzeichnis ausführen. Migration `0068_email_entity_suggestions` ergänzt den Ergebnisspeicher. Migration `0069_reset_email_entity_suggestions` verwirft bisherige KI-Vorschläge, damit sie beim nächsten Öffnen ohne Empfängerbelege neu berechnet werden. Bestehende Verknüpfungen bleiben erhalten. Die Erkennung nutzt wie die bestehende Rechnungserkennung den konfigurierten zentralen KI-Dienst oder `OPENAI_API_KEY`, mit dem im Erkennungsdienst konfigurierten Modell. Ohne KI-Konfiguration erscheint eine Meldung im Postfach. Vor dem Einsatz die Backend-Migrationen mit `npm run migrate` im Backend-Verzeichnis ausführen. Migration `0068_email_entity_suggestions` ergänzt den Ergebnisspeicher. Migration `0069_reset_email_entity_suggestions` verwirft bisherige KI-Vorschläge, damit sie beim nächsten Öffnen ohne Empfängerbelege neu berechnet werden. Bestehende Verknüpfungen bleiben erhalten. Die Erkennung nutzt wie die bestehende Rechnungserkennung den konfigurierten zentralen KI-Dienst oder `OPENAI_API_KEY`. Sie fragt standardmäßig `gpt-5.6-luna` an. Lehnt ein älterer zentraler Dienst dieses Modell als inkompatibel ab, wird der Aufruf einmalig mit `gpt-4o-mini` wiederholt. Ohne KI-Konfiguration erscheint eine Meldung im Postfach.
Empfänger- und CC-Kopfdaten werden weder für die Vorauswahl noch als KI-Eingabe verwendet. Empfängerangaben in zitierten Mailköpfen dürfen laut Analyseanweisung keine Zuordnung begründen. Übermittelt werden Betreff, Absender und bis zu 16.000 Zeichen Mailtext (ersatzweise HTML ohne Tags oder Vorschautext). Anhänge werden nicht analysiert. Stammdaten werden auf den aktuellen Mandanten und nicht archivierte Einträge begrenzt; übertragen werden Name, Typ, ID, Nummer und vorhandene E-Mail-Adressen. Bei mehr als 150 Einträgen priorisiert eine lokale Vorauswahl passende Adressen, Nummern und Namen. Dadurch können bei großen Datenbeständen rein semantische Zusammenhänge außerhalb dieser Vorauswahl unentdeckt bleiben. Empfänger- und CC-Kopfdaten werden weder für die Vorauswahl noch als KI-Eingabe verwendet. Empfängerangaben in zitierten Mailköpfen dürfen laut Analyseanweisung keine Zuordnung begründen. Übermittelt werden Betreff, Absender und bis zu 16.000 Zeichen Mailtext (ersatzweise HTML ohne Tags oder Vorschautext). Anhänge werden nicht analysiert. Stammdaten werden auf den aktuellen Mandanten und nicht archivierte Einträge begrenzt; übertragen werden Name, Typ, ID, Nummer und vorhandene E-Mail-Adressen. Bei mehr als 150 Einträgen priorisiert eine lokale Vorauswahl passende Adressen, Nummern und Namen. Dadurch können bei großen Datenbeständen rein semantische Zusammenhänge außerhalb dieser Vorauswahl unentdeckt bleiben.