Moderation Report Triage: Node.js LLM API JSON Contracts for Portable Summaries A developer outlines a portable design for fintech moderation reports, recommending a narrow JSON contract at the Node.js API boundary to keep human-review queues structured. The approach defines a strict schema with fields like title, summary, bullets, and action items, and uses an adapter to isolate provider-specific details, enabling easy provider swaps. The developer emphasizes validating the contract after every LLM call and distinguishing between 'no such fact' and 'classifier unsure' states. Fintech moderation reports should not enter a human-review queue as a blob of model prose. The portable design is a narrow JSON contract at the Node.js API boundary: title, summary, bullets, and action items, with an explicit result for uncertainty. Keep the model provider behind one adapter, then test that contract against several providers before choosing a default. Short answer: define the JSON shape first, validate it after every LLM call, and make provider replacement an adapter change rather than an application rewrite. This is slower to sketch than response.text , but much faster to operate once reports feed queues, dashboards, and audit records. | Integration shape | Use it when | The catch | |---|---|---| | One provider SDK throughout the service | A provider-specific capability is the product requirement | Provider changes touch application code and tests | | A common HTTP adapter | Portability and time-to-first-call matter | The adapter must translate request and response differences | | A self-hosted model endpoint | Data locality and deployment control dominate | The team owns capacity, upgrades, and model evaluation | | Manual review with model suggestions | Evidence is sparse or the decision is high risk | Review volume stays high and automation gains are limited | That is the choice matrix. The recommendation is the middle row for this scenario: a small adapter plus a strict internal schema. It gives a fintech review service one place to swap credentials, model names, timeouts, and response parsing. The adapter is not a magic compatibility layer. It is a deliberately boring boundary. No prose parser. Start with the reviewer, not the model. A reviewer needs a short title, a plain summary, evidence bullets, a moderation category, and tasks that can be assigned. The contract should also preserve the distinction between “the report contains no such fact” and “the classifier is unsure.” Those are different states. type ModerationSummary = { title: string; summary: string; bullets: string ; category: "fraud" | "abuse" | "privacy" | "other" | "unclear"; confidence: "high" | "medium" | "low"; action items: Array<{ text: string; owner: string | null; due: string | null; } ; }; const summarySchema = { type: "object", additionalProperties: false, required: "title", "summary", "bullets", "category", "confidence", "action items" , properties: { title: { type: "string", minLength: 1 }, summary: { type: "string", minLength: 1 }, bullets: { type: "array", items: { type: "string" } }, category: { type: "string", enum: "fraud", "abuse", "privacy", "other", "unclear" , }, confidence: { type: "string", enum: "high", "medium", "low" }, action items: { type: "array", items: { type: "object", additionalProperties: false, required: "text", "owner", "due" , properties: { text: { type: "string" }, owner: { type: "string", "null" }, due: { type: "string", "null" }, }, }, }, }, } as const; null is intentional. If a report does not name an owner or due date, the service must not manufacture one. unclear is intentional too. A model that cannot classify a report should create a review decision, not a confident-looking label. Keep provider-specific request details in one function. The rest of the service should know about ModerationSummary , not about a particular SDK response object. This example uses an injected HTTP endpoint, so the same application code can point at a hosted or self-hosted implementation without changing the domain contract. type LlmClient = input: { system: string; user: string; schema: typeof summarySchema; } = Promise