feat: enhance logging and moderation response handling with improved serialization and error management

This commit is contained in:
MythEclipse
2026-05-30 20:13:44 +07:00
parent b105f9748a
commit b19529f135
10 changed files with 177 additions and 141 deletions
+1 -1
View File
@@ -29,7 +29,7 @@ const consoleFormat = winston.format.printf((info) => {
const { level, message, timestamp, context, ...metadata } = info;
const contextLabel = context ? ` [${String(context)}]` : "";
const metadataText = Object.keys(metadata).length
? ` ${JSON.stringify(metadata)}`
? ` ${JSON.stringify(formatLogMetadata(metadata))}`
: "";
return `${timestamp} ${level}${contextLabel}: ${message}${metadataText}`;
+24 -3
View File
@@ -50,7 +50,12 @@ const isPlainObject = (value: unknown): value is Record<string, unknown> => {
return prototype === Object.prototype || prototype === null;
};
export const serializeLogValue = (value: unknown): unknown => {
export const serializeLogValue = (
value: unknown,
_seen: WeakSet<object> = new WeakSet(),
): unknown => {
if (value === null || value === undefined) return value;
if (value instanceof Error) {
return serializeError(value);
}
@@ -63,19 +68,35 @@ export const serializeLogValue = (value: unknown): unknown => {
return value.toString();
}
if (typeof value === "object") {
if (_seen.has(value as object)) {
return "[Circular]";
}
_seen.add(value as object);
}
if (Array.isArray(value)) {
return value.map(serializeLogValue);
return value.map((item) => serializeLogValue(item, _seen));
}
if (isPlainObject(value)) {
return Object.fromEntries(
Object.entries(value).map(([key, nestedValue]) => [
key,
serializeLogValue(nestedValue),
serializeLogValue(nestedValue, _seen),
]),
);
}
// Non-plain objects (ClientRequest, IncomingMessage, etc.) — serialize as safe string
if (typeof value === "object") {
try {
return `[Object ${(value as any)?.constructor?.name ?? "unknown"}]`;
} catch {
return "[Object]";
}
}
return value;
};
+43 -26
View File
@@ -33,22 +33,22 @@ const RecommendedActionSchema = z.enum([
"escalate",
]);
const ResultItemSchema = z.object({
message_id: z.union([z.string(), z.number()]).transform(String),
status: z.enum(["clean", "warn", "flagged"]),
flags: z.array(z.string()).optional(),
score: z.number(),
analysis: z.string().nullable().optional(),
categories: z.array(z.string()).optional(),
severity: SeveritySchema.optional(),
confidence: z.number().optional(),
recommended_action: RecommendedActionSchema.optional(),
policy_version: z.string().optional(),
evidence: z.array(z.string()).optional(),
});
const ModerationResponseSchema = z.object({
results: z.array(
z.object({
message_id: z.union([z.string(), z.number()]).transform(String),
status: z.enum(["clean", "warn", "flagged"]).catch("clean"),
flags: z.array(z.string()).catch([]),
score: z.number().catch(0),
analysis: z.string().catch(""),
categories: z.array(z.string()).optional().catch(undefined),
severity: SeveritySchema.optional().catch(undefined),
confidence: z.number().optional().catch(undefined),
recommended_action: RecommendedActionSchema.optional().catch(undefined),
policy_version: z.string().optional().catch(undefined),
evidence: z.array(z.string()).optional().catch(undefined),
}),
),
results: z.array(ResultItemSchema),
});
const log = createChildLogger("llmModerationClient");
@@ -232,13 +232,26 @@ export function parseModerationResponse(
if (Array.isArray(parsed)) {
parsed = { results: parsed };
} else if (parsed && typeof parsed === "object" && !("results" in parsed)) {
const arrayKey = Object.keys(parsed).find((key) =>
Array.isArray((parsed as any)[key]),
);
if (arrayKey) {
parsed.results = (parsed as any)[arrayKey];
} else {
// If the object directly looks like a result item (has message_id), wrap it
// BEFORE checking for array keys. This prevents flags:[] from being
// mistaken as the results array on a single-object response.
if ("message_id" in parsed) {
parsed = { results: [parsed] };
} else {
// Find the first non-empty array key whose elements are objects with message_id
const arrayKey = Object.keys(parsed).find((key) => {
const val = (parsed as any)[key];
return (
Array.isArray(val) &&
val.length > 0 &&
val.every((item: unknown) => typeof item === "object" && item !== null && "message_id" in (item as any))
);
});
if (arrayKey) {
parsed.results = (parsed as any)[arrayKey];
} else {
parsed = { results: [parsed] };
}
}
}
@@ -272,12 +285,16 @@ export function parseModerationResponse(
}
if (foundIds.has(finalId)) {
return null; // Ignore duplicates safely
throw new Error(
`Duplicate message_id in moderation response: ${finalId}`,
);
}
foundIds.add(finalId);
if (hasDeferralAnalysis(analysis)) {
const coalescedAnalysis = analysis ?? "";
if (hasDeferralAnalysis(coalescedAnalysis)) {
throw new Error(
`Deferral analysis is not allowed for message ${finalId}; return a direct moderation decision`,
);
@@ -291,10 +308,10 @@ export function parseModerationResponse(
return {
messageId: finalId,
status: status as "clean" | "warn" | "flagged",
flags,
flags: flags ?? [],
score: normalizedScore,
analysis,
categories: categories ?? flags,
analysis: coalescedAnalysis,
categories: categories ?? (flags ?? []),
severity: normalizedSeverity,
confidence: normalizedConfidence,
recommendedAction:
+4
View File
@@ -185,6 +185,10 @@ export async function updateMessageAsEdited(
ai_moderation_flags: null,
ai_moderation_score: null,
ai_analysis: null,
ai_categories: null,
ai_severity: null,
ai_confidence: null,
ai_recommended_action: null,
ai_analyzed_at: null,
ai_error: null,
})