Six independent functions that handle the hard parts of working with LLM output. Use them inside enforce, or drop any one into an existing pipeline.
(raw: string) => unknown
Normalizes raw LLM output into clean JSON. Strips markdown fences, extracts JSON from prose wrapping, and coerces string-typed values to their correct types.
Markdown fences
clean('```json\n{"score": 85}\n```');
// { score: 85 }
Prose wrapping
clean('Here you go: {"score": "85"}');
// { score: 85 }
Type coercion
clean('{"active": "true", "count": "3"}');
// { active: true, count: 3 }
Language variations
clean('```JSON\n{"score": 85}\n```');
// { score: 85 }
This runs automatically inside enforce. Use it standalone when you already have raw output and want clean JSON.
(data, schema, invariants?) => Result<T>
Validates data against a Zod schema and optional invariants. Deterministic — no LLM involved. Returns typed data or a structured error with every issue listed.
Pass
check({ name: "Alice", age: 30 }, Schema);
// { ok: true, data: { name: "Alice", age: 30 } }
Fail — schema violation
check({ name: "Alice", age: -5 }, Schema);
// { ok: false, error: { issues: ["age: must be >= 0 (received -5)"] } }
Fail — missing field
check({ name: 123 }, Schema);
// { ok: false, error: { issues: ["name: expected string, got number", "age: required"] } }
(raw, cleaned) => FailureCategory
Categorizes a failed LLM response into one of 8 failure types. Used internally by enforce to generate the right repair message for each situation.
| Category | What happened | Default repair |
|---|---|---|
| EMPTY_RESPONSE | Model returned nothing | Ask for JSON matching the schema |
| REFUSAL | Model declined ("I'm sorry", "as an AI") | Redirect to structured data task |
| NO_JSON | Response contained no JSON at all | Ask for JSON only, no prose |
| TRUNCATED | JSON cut off (unbalanced braces) | Ask for a shorter, complete response |
| PARSE_ERROR | JSON present but malformed | Ask for strictly valid JSON |
| VALIDATION_ERROR | Valid JSON but failed schema | List the specific Zod errors |
| INVARIANT_ERROR | Correct types but failed constraints | List the specific violations |
| RUN_ERROR | Your function threw an exception | Ask to try again |
(detail, repairs?) => Message[] | false
Turns validation failures into targeted repair messages for the next attempt. The output is an array of messages you append to the conversation — the model gets specific feedback about what went wrong.
fix(checkResult.error);
// [{
// role: "user",
// content: "Your previous response had validation errors:\n"
// + "- age: must be >= 0 (received -5)\n"
// + "- email: expected string, got undefined\n"
// + "Please correct these issues and respond with valid JSON."
// }]
You can override or disable repair for any category. Pass false to stop retrying a specific failure type.
(state, schema) => Record<string, unknown>
Strips a state object down to only the fields the schema defines. Prevents sending PII, secrets, or unrelated data to the LLM.
const fullUser = {
name: "Alice Chen",
email: "alice@company.com",
ssn: "123-45-6789",
passwordHash: "$2b$10$abc...",
loginHistory: [{ ts: "2024-01-01", ip: "10.0.0.1" }],
};
select(fullUser, z.object({ name: z.string(), email: z.string() }));
// { name: "Alice Chen", email: "alice@company.com" }
//
// ssn, passwordHash, loginHistory — gone.
// The LLM never sees them.
(schema: ZodType) => string
Generates format instructions from a Zod schema. Used internally by enforce to populate attempt.prompt. Use it directly when building prompts manually.
import { prompt } from "llm-contract";
import { z } from "zod";
const instructions = prompt(
z.object({
sentiment: z.enum(["positive", "negative", "neutral"]),
confidence: z.number().min(0).max(1),
})
);
// Generates format instructions describing the expected JSON shape,
// field types, enums, and constraints — ready to use as a system message.
enforce runs the full loop. But every primitive is exported independently — use whichever pieces fit your pipeline.
import { clean, check, fix, classify } from "llm-contract";
const raw = await yourLLMCall();
const cleaned = clean(raw);
const result = check(cleaned, schema, invariants);
if (!result.ok) {
const category = classify(raw, cleaned);
const repairs = fix(result.error);
// feed repairs back into your own retry logic
}