Before/after comparisons showing what changes when you use llm-contract.
Parse LLM output into a typed sentiment + confidence score.
async function analyzeSentiment(text: string) {
const response = await openai.chat.completions.create({
model: "gpt-4o-mini",
messages: [{
role: "system",
content: "Analyze the sentiment. Return JSON with: "
+ "sentiment (positive/negative/neutral), "
+ "confidence (0-1). Return ONLY JSON.",
}, { role: "user", content: text }],
});
const raw = response.choices[0].message.content ?? "";
let cleaned = raw
.replace(/```json\n?/g, "")
.replace(/```\n?/g, "")
.trim();
let parsed;
try {
parsed = JSON.parse(cleaned);
} catch {
throw new Error(`Invalid JSON: ${raw}`);
}
if (!["positive", "negative", "neutral"]
.includes(parsed.sentiment)) {
throw new Error(`Bad sentiment: ${parsed.sentiment}`);
}
if (typeof parsed.confidence !== "number"
|| parsed.confidence < 0
|| parsed.confidence > 1) {
throw new Error(`Bad confidence: ${parsed.confidence}`);
}
return parsed as {
sentiment: "positive" | "negative" | "neutral";
confidence: number;
};
}
import { enforce } from "llm-contract";
import { z } from "zod";
const Sentiment = z.object({
sentiment: z.enum(["positive", "negative", "neutral"]),
confidence: z.number().min(0).max(1),
});
async function analyzeSentiment(text: string) {
return enforce(Sentiment, async (attempt) => {
const res = await openai.chat.completions.create({
model: "gpt-4o-mini",
messages: [
{ role: "system", content: attempt.prompt },
{ role: "user", content: text },
...attempt.fixes,
],
});
return res.choices[0].message.content;
});
}
What changed
attempt.prompt auto-generated from schema — always in syncclean runs automatically — handles fences, prose, type coercionattempt.fixes contains repair on retryResult<T> — real types, no castExtract structured data from PDFs with cross-field math invariants.
async function extractInvoice(pdfText: string) {
let lastError: string | undefined;
for (let attempt = 0; attempt < 3; attempt++) {
const messages: Array<{ role: string; content: string }> = [
{
role: "system",
content: `Extract these fields as JSON:
- vendor (string)
- invoiceNumber (string)
- date (YYYY-MM-DD)
- lineItems (array: description, quantity,
unitPrice, amount)
- subtotal, tax, total (numbers)
Return ONLY valid JSON.`,
},
{ role: "user", content: pdfText },
];
if (attempt > 0 && lastError) {
messages.push({
role: "user",
content: `Previous response was invalid:
${lastError}. Please fix.`,
});
}
const res = await openai.chat.completions.create({
model: "gpt-4o",
messages: messages as any,
});
const raw = res.choices[0].message.content ?? "";
let parsed: any;
try {
parsed = JSON.parse(
raw.replace(/```json\n?/g, "")
.replace(/```\n?/g, "").trim()
);
} catch {
lastError = "Not valid JSON";
continue;
}
if (typeof parsed.total !== "number") {
lastError = "total must be a number";
continue;
}
return parsed;
}
throw new Error(`Failed after 3 attempts: ${lastError}`);
}
import { enforce } from "llm-contract";
import { z } from "zod";
const Invoice = z.object({
vendor: z.string(),
invoiceNumber: z.string(),
date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
lineItems: z.array(z.object({
description: z.string(),
quantity: z.number(),
unitPrice: z.number(),
amount: z.number(),
})),
subtotal: z.number(),
tax: z.number(),
total: z.number(),
});
async function extractInvoice(pdfText: string) {
return enforce(Invoice, async (attempt) => {
const res = await openai.chat.completions.create({
model: "gpt-4o",
messages: [
{ role: "system", content: attempt.prompt },
{ role: "user", content: pdfText },
...attempt.fixes,
],
});
return res.choices[0].message.content;
}, {
invariants: [
(d) => {
const sum = d.lineItems.reduce(
(s, i) => s + i.amount, 0
);
return Math.abs(sum - d.subtotal) < 0.01
|| `line items sum to ${sum}, subtotal is ${d.subtotal}`;
},
(d) => Math.abs(d.subtotal + d.tax - d.total) < 0.01
|| `subtotal + tax != total`,
],
});
}
What changed
clean coerces "250.00" to 250 automaticallyline items sum to 400, but subtotal is 350Classify support tickets with constrained enums and bounded arrays.
async function classifyTicket(ticket: string) {
const response = await openai.chat.completions.create({
model: "gpt-4o-mini",
messages: [{
role: "system",
content: `Classify this ticket as JSON:
- category: "bug", "feature", "question", "billing"
- priority: "low", "medium", "high", "critical"
- tags: array of relevant tags (max 5)
JSON only.`,
}, { role: "user", content: ticket }],
});
const raw = response.choices[0].message.content ?? "";
const cleaned = raw
.replace(/```json\n?/g, "")
.replace(/```/g, "").trim();
const parsed = JSON.parse(cleaned);
if (!["bug", "feature", "question", "billing"]
.includes(parsed.category)) {
throw new Error(`Invalid category: ${parsed.category}`);
}
// Priority validation missing
// Tags length check missing
return parsed;
}
import { enforce } from "llm-contract";
import { z } from "zod";
const Ticket = z.object({
category: z.enum([
"bug", "feature", "question", "billing",
]),
priority: z.enum([
"low", "medium", "high", "critical",
]),
tags: z.array(z.string()).max(5),
});
async function classifyTicket(ticket: string) {
return enforce(Ticket, async (attempt) => {
const res = await openai.chat.completions.create({
model: "gpt-4o-mini",
messages: [
{ role: "system", content: attempt.prompt },
{ role: "user", content: ticket },
...attempt.fixes,
],
});
return res.choices[0].message.content;
});
}
What changed
"urgent" is caught and repaired, not silently acceptedEvery example follows the same shape:
const result = await enforce(schema, async (attempt) => {
const { text } = await yourLLMCall({
system: attempt.prompt, // from schema
messages: attempt.fixes, // from previous failure
});
return text;
}, {
invariants: [
// cross-field rules Zod can't express
],
onAttempt: (event) => {
// observe every attempt — log, trace, alert
},
});
schema — Zod schema, single source of truthattempt.prompt — auto-generated format instructionsattempt.fixes — targeted repair from previous failuresinvariants — cross-field checks that compound correctnessFull example files with simulated failures to show the contract loop in action:
extraction.ts
Invoice extraction with math invariants
classification.ts
Ticket classification with enums
moderation.ts
Content moderation with confidence gates
scoring.ts
Scoring with numeric constraints
fallback.ts
Graceful fallback on exhausted retries
primitives.ts
Using clean, check, fix, select individually