fix(ai-moderation): bedah delay attachment ~330s -> target <20s

Root cause (trace msg 1541417073245290638):
- Race-guard upload-pending balik results:[] diperlakukan sbg SUKSES
  -> row yatam 'processing' sampai cleanup 300s mengembalikan
- Vision gagal 3x utk GIF besar (SSE truncation) tanpa fallback

Fix:
- Sinyal eksplisit uploadPending dari worker race guard
- Classifier murni classifyIndividualWorkerResult(): upload_pending ->
  requeue pending + reschedule segera (250ms), bukan error palsu;
  empty-results ok:true kini error transien (bug silent-success mati)
- llmVision fallback stream:false sekali saat SSE truncation
- Safety-net cleanup stuck processing 300s -> 120s
This commit is contained in:
asepharyana
2026-08-24 22:05:28 +07:00
parent fca96396b9
commit 842610b1af
7 changed files with 306 additions and 17 deletions
@@ -356,10 +356,10 @@ export async function llmVision(
promptText: string,
imageUrl: { url: string },
): Promise<string | null> {
const completion = await llmChat({
const params = {
messages: [
{
role: "user",
role: "user" as const,
content: [
{ type: "text" as const, text: promptText },
{ type: "image_url" as const, image_url: imageUrl },
@@ -371,9 +371,26 @@ export async function llmVision(
temperature: 0.1,
top_p: 0.9,
retries: 0,
stream: true, // router always streams SSE; non-stream waits for full body and times out
timeout: config.AI_LLM_VISION_ANALYSIS_TIMEOUT_MS ?? 60_000,
});
};
// Streaming first (the router always streams SSE; a non-stream request
// waits for the full body and times out on slow models). Fallback (2026-08-24):
// large GIFs/images sometimes get their SSE stream truncated mid-flight by
// the upstream ("Stream ended before producing a non-ping SSE event") — all
// streaming retries fail identically, so retry ONCE with stream:false where
// the router assembles the complete response server-side.
let completion;
try {
completion = await llmChat({ ...params, stream: true });
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
if (/stream ended before producing a non-ping sse/i.test(msg)) {
completion = await llmChat({ ...params, stream: false });
} else {
throw err;
}
}
if (!completion) return null;
return completion.choices[0]?.message?.content?.trim() ?? null;