fix(ai-moderation): add Furina/Genshin character name exception to prevent false positive furry flags

Nama karakter game/anime populer seperti 'Furina' dari Genshin Impact
sering kena false positive sebagai 'sexual_deviation' karena kemiripan
fonetik dengan kata 'furry'. Menambahkan aturan eksplisit bahwa nama
karakter fiksi normal bukan referensi furry fetish, dgn pengecualian
jika konteks pesan secara eksplisit membahas aspek fetish/seksual.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
MythEclipse
2026-06-06 12:04:10 +07:00
co-authored by Claude Opus 4.8
parent 25c86dcc30
commit 817f1ce3df
4 changed files with 85 additions and 33 deletions
@@ -2,7 +2,6 @@ import { createChildLogger } from "@bete/shared/logger";
import { delay, retryWithBackoff } from "@bete/shared/utils";
import { LRUCache } from "lru-cache";
import type { ChatCompletion } from "openai/resources/chat/completions";
import { AbortError } from "p-retry";
import { z } from "zod";
import { config } from "../../shared/config/config.js";
import { resizeImageForVision } from "../attachment-upload/imageResizer.js";
@@ -758,6 +757,7 @@ async function callModerationLLM(
buildContent: (state: RetryState) => Promise<string>,
targetIds: string[],
label: string,
signal?: AbortSignal,
): Promise<{
results: AnalysisResult[];
raw: ChatCompletion | null;
@@ -781,6 +781,7 @@ async function callModerationLLM(
max_tokens: 16384,
jsonResponse: { type: "json_object" },
retries: 0,
signal,
});
if (!completion) {
@@ -848,7 +849,9 @@ async function callModerationLLM(
apiError?.status === 401 ||
apiError?.status === 403
) {
throw new AbortError(apiError);
const abortErr = new Error(String(apiError));
abortErr.name = "AbortError";
throw abortErr;
}
// 5xx server errors → retryable transient errors
// p-retry will retry these; on final exhaustion the outer catch
@@ -870,11 +873,16 @@ async function callModerationLLM(
minTimeout: 5_000,
maxTimeout: 60_000,
factor: 3,
signal,
},
);
parsed = analysis.parsed;
result = analysis.result;
} catch (err) {
if (err instanceof Error && err.name === "AbortError") {
throw err;
}
const errorMsg = err instanceof Error ? err.message : String(err);
const isApiError = !state.lastInvalidContent;
@@ -1173,21 +1181,25 @@ async function runTextOnlyBatch(
return `${systemText}\n\n<messages_to_analyze>\n${messagesBlock}\n</messages_to_analyze>`;
};
const batchResult = (await Promise.race([
callModerationLLM(buildContent, targetIds, `text-batch-${i + 1}`),
new Promise<{ results: AnalysisResult[]; raw: unknown }>((_, reject) => {
const timeout = setTimeout(
() =>
reject(
new Error(
`Text-only batch sub-batch ${i + 1} timed out for messages ${targetIds.join(", ")}`,
),
),
timeoutMs,
const abortController = new AbortController();
const timeoutId = setTimeout(() => {
abortController.abort();
}, timeoutMs);
timeoutId.unref();
let batchResult: { results: AnalysisResult[]; raw: unknown };
try {
batchResult = await callModerationLLM(buildContent, targetIds, `text-batch-${i + 1}`, abortController.signal);
} catch (err: any) {
if (err.name === "AbortError" || abortController.signal.aborted) {
throw new Error(
`Text-only batch sub-batch ${i + 1} timed out for messages ${targetIds.join(", ")}`,
);
timeout.unref();
}),
])) as { results: AnalysisResult[]; raw: unknown };
}
throw err;
} finally {
clearTimeout(timeoutId);
}
const rawUsage = (
batchResult.raw as {
@@ -1269,27 +1281,31 @@ async function runSingleMediaAnalysis(
// Timeout wrapper (R4)
const timeoutMs = config.AI_LLM_MEDIA_ANALYSIS_TIMEOUT_MS ?? 60000;
return Promise.race([
_runSingleMediaAnalysis(
const abortController = new AbortController();
const timeoutId = setTimeout(() => {
abortController.abort();
}, timeoutMs);
timeoutId.unref();
try {
return await _runSingleMediaAnalysis(
target,
contextText,
allAttachments,
targetId,
targetIds,
),
new Promise<{ results: AnalysisResult[]; raw: unknown }>((_, reject) => {
const timeout = setTimeout(
() =>
reject(
new Error(
`Media analysis timed out after ${timeoutMs}ms for message ${targetId}`,
),
),
timeoutMs,
abortController.signal,
);
} catch (err: any) {
if (err.name === "AbortError" || abortController.signal.aborted) {
throw new Error(
`Media analysis timed out after ${timeoutMs}ms for message ${targetId}`,
);
timeout.unref();
}),
]);
}
throw err;
} finally {
clearTimeout(timeoutId);
}
}
async function _runSingleMediaAnalysis(
@@ -1298,6 +1314,7 @@ async function _runSingleMediaAnalysis(
allAttachments: AttachmentRecord[] | undefined,
targetId: string,
targetIds: string[],
signal?: AbortSignal,
): Promise<{ results: AnalysisResult[]; raw: unknown }> {
// Lazy init sticker cache
if (!isStickerCacheReady()) {
@@ -1694,6 +1711,7 @@ async function _runSingleMediaAnalysis(
async (_state: RetryState) => userContent,
targetIds,
`media:${targetId}`,
signal,
);
return result;