feat: add response logger for moderation/vision audit trail + cache model versioning

- Add responseLogger.ts with comprehensive logging: vision analysis, moderation
  analysis batches, cache hit/miss events, errors, retries, false positives,
  model version changes, cache invalidation
- Integrate responseLogger calls into llmModerationClient.ts (cache events,
  moderation results, errors, retries)
- Add model_version column to text_analysis_cache schema with composite
  index for efficient filtering
- Add migration SQL for model_version column
- Document CACHE_MODEL_VERSION in .env.example
This commit is contained in:
MythEclipse
2026-06-02 12:23:52 +07:00
parent f4db42e11c
commit 385fc0bbbf
6 changed files with 505 additions and 18 deletions
@@ -32,8 +32,17 @@ import {
makeImageCacheKey,
makeStickerCacheKey,
upsertCachedMediaAnalysis,
VISION_MODEL_VERSION,
} from "./textCacheStore.js";
import { extractUrlsFromText, fetchUrlSafely } from "./urlFetcher.js";
import {
logVisionAnalysis,
logCacheEvent,
logModerationAnalysis,
logModerationError,
logVisionError,
logRetryAttempt,
} from "./responseLogger.js";
const SeveritySchema = z.enum(["none", "low", "medium", "high", "critical"]);
const RecommendedActionSchema = z.enum([
@@ -786,6 +795,13 @@ async function callModerationLLM(
`Robust Fallback (${label}): Failed to parse moderation response. Marking all targets as analysis errors.`,
);
// Log error with responseLogger
logModerationError(targetIds, config.AI_LLM_MODEL, parseError, {
phase: "parse_response",
label,
contentLength: state.lastInvalidContent.length,
});
// Sanitized error messages — no internal details exposed (R10)
const errorCode = `MOD_${Date.now().toString(36).slice(0, 6)}`;
parsed = targetIds.map((id) => ({
@@ -896,6 +912,21 @@ async function runTextOnlyBatch(
allResults.push(...batchResult.results);
if (batchResult.raw) lastRaw = batchResult.raw;
// Log batch results with comprehensive details
logModerationAnalysis(
targetIds,
config.AI_LLM_MODEL,
batchResult.results,
0, // Duration will be tracked at higher level
batchResult.raw?.usage
? {
prompt_tokens: batchResult.raw.usage.prompt_tokens,
completion_tokens: batchResult.raw.usage.completion_tokens,
total_tokens: batchResult.raw.usage.total_tokens,
}
: undefined,
);
}
log.info(
@@ -1012,6 +1043,7 @@ async function _runSingleMediaAnalysis(
{ attachmentId: att.id, cacheKey: attVisionKey },
"Vision cache HIT for attachment — skipped download",
);
logCacheEvent("hit", attVisionKey, "media", VISION_MODEL_VERSION);
const sourceLabel = `[gambar di atas adalah attachment ${att.filename} dari pesan id=${att.message_id}]`;
const analysisText = `[Media analysis for message ${att.message_id}] ${sourceLabel}: ${cachedVision}`;
const existing = mediaAnalysisMap.get(targetId) ?? [];
@@ -1020,6 +1052,8 @@ async function _runSingleMediaAnalysis(
return;
}
logCacheEvent("miss", attVisionKey, "media", VISION_MODEL_VERSION);
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 15000);