fix(ai-moderation): never cache vision outputs that claim 'no image seen'
Root cause (3rd layer after50371bd+4f4c435): a vision model run (2026-08-10) returned 'Maaf, saya tidak melihat gambar apapun yang terlampir...' and that text was cached as a VALID vision_llm result (image + phash keys, 24h/7d TTL). Every subsequent analysis of the same image (same hash/phash) hit the poisoned cache, so image analysis looked broken forever even though 9router responded fine — the moderation LLM wrote 'lampiran yang gagal terbaca' from a cache hit. Also: mimo via 9router streams reasoning in delta.reasoning + delta.reasoning_details[].text (content:"") — extractChunkText only read delta.reasoning_content, so those runs aggregated empty → 'Vision API null response' (observed 08:54/09:07/09:38). Fixes: - llmClient.extractChunkText: fall back to delta.reasoning and reasoning_details[].text (mimo), on top of reasoning_content (gemma). - visionAnalyzer: isNoImageSeenText() detects 'no image' style outputs; such results are NEVER cached, and poisoned entries are purged when hit (LRU/DB/phash) so re-analysis actually re-runs vision. - Tests: reasoning/reasoning_details extraction + isNoImageSeenText (Indonesian + English, no false positives on real descriptions).
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// isNoImageSeenText — vision outputs that claim "no image" must not be cached
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// Regression (2026-08-11): the vision model sometimes answered "Maaf, saya
|
||||
// tidak melihat gambar apapun yang terlampir..." and that text was cached as
|
||||
// a VALID vision_llm result. Every later analysis of the same image (same
|
||||
// hash / phash) then hit the poisoned cache and the moderation LLM wrote
|
||||
// "lampiran yang gagal terbaca" — image analysis seemed permanently broken
|
||||
// even though 9router was responding fine.
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { isNoImageSeenText } from "../src/modules/ai-moderation/visionAnalyzer.js";
|
||||
|
||||
describe("isNoImageSeenText — poisoned vision output detection", () => {
|
||||
it("detects the exact poisoned strings seen in production", () => {
|
||||
expect(
|
||||
isNoImageSeenText(
|
||||
"Maaf, saya tidak melihat gambar apapun yang terlampir dalam pesan Anda. Mohon kirimkan ulang gambarnya agar saya bisa mendeskripsikannya secara objektif dan spesifik.",
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
isNoImageSeenText(
|
||||
"Tidak ada gambar yang terlampir. Tidak bisa deskripsi tanpa input visual.",
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("detects English variants", () => {
|
||||
expect(isNoImageSeenText("I cannot see any image in this message")).toBe(
|
||||
true,
|
||||
);
|
||||
expect(isNoImageSeenText("No image provided")).toBe(true);
|
||||
expect(isNoImageSeenText("there is no image attached")).toBe(true);
|
||||
expect(isNoImageSeenText("I don't see an image")).toBe(true);
|
||||
});
|
||||
|
||||
it("does NOT flag legitimate image descriptions", () => {
|
||||
expect(
|
||||
isNoImageSeenText(
|
||||
"Gambar ini menampilkan dua panel komik, seorang gadis berambut biru tersipu saat dipuji.",
|
||||
),
|
||||
).toBe(false);
|
||||
expect(
|
||||
isNoImageSeenText("Ini adalah screenshot dari sebuah website rekrutmen."),
|
||||
).toBe(false);
|
||||
expect(isNoImageSeenText("Emoji menampilkan ekspresi wajah tertawa.")).toBe(
|
||||
false,
|
||||
);
|
||||
expect(isNoImageSeenText(null)).toBe(false);
|
||||
expect(isNoImageSeenText(undefined)).toBe(false);
|
||||
expect(isNoImageSeenText("")).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user