feat: enhance logging and moderation response handling with improved serialization and error management
This commit is contained in:
+1
-1
@@ -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}`;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user