@painitehq/structured-llm
v0.3.2
Published
Force LLM output into structured, type-safe JSON. Stop your app from crashing on malformed AI responses.
Maintainers
Readme
@painitehq/structured-llm
Force LLM output into structured, type-safe JSON. Stop your app from crashing on malformed AI responses.
Install
npm install @painitehq/structured-llm
# or
bun add @painitehq/structured-llmQuick Start
import { extract, defineSchema } from "@painitehq/structured-llm";
const schema = defineSchema("invoice", {
invoiceNumber: { type: "string" },
totalAmount: { type: "number" },
vendor: { type: "string" },
items: { type: "array", items: { type: "string" } },
});
const result = await extract(messyText, schema, {
apiKey: "your-openrouter-api-key",
});
console.log(result.data);
// { invoiceNumber: "INV-2024-042", totalAmount: 500, vendor: "Acme Corp", items: [...] }
console.log(result.confidence); // 100
console.log(result.attempts); // 1What It Does
LLMs return unstructured text. Sometimes it's valid JSON. Sometimes it's wrapped in markdown. Sometimes it's completely broken. This SDK:
- Forces the model to output valid JSON via strict prompt engineering
- Repairs malformed JSON (trailing commas, missing brackets, broken quotes)
- Unwraps named wrappers like
{"invoice": {...}}→{...} - Validates the output against your schema
- Coerces wrong types (
"42"→42,"true"→true) - Fills missing fields with sensible defaults
- Retries with escalating instructions if the model fails
Features
Forced Structured Output
The SDK doesn't ask the model to "give JSON". It forces it:
CRITICAL RULES:
- Output ONLY valid JSON. No text before or after.
- No markdown. No code blocks. No explanations.
- Every field MUST be present with the correct type.Escalating Retries
If the model fails, the SDK retries with increasingly strict instructions:
- Attempt 1: Clean forced prompt
- Attempt 2: "Your response was invalid. Here's the error. Fix it."
- Attempt 3: "FINAL ATTEMPT. THIS IS YOUR LAST CHANCE."
Post-Validation Repair
Even if the JSON parses, the SDK fixes type mismatches:
| Model returns | Schema expects | SDK does |
|---------------|----------------|----------|
| "42" | number | coerces to 42 |
| "true" | boolean | coerces to true |
| 42 | string | coerces to "42" |
| missing field | any type | fills with default |
Confidence Scoring
Every extraction returns a confidence score (0-100):
const result = await extract(text, schema, { apiKey });
result.confidence; // 85
result.repairLog; // [{ type: "type_coercion", detail: "Coerced \"price\" to number" }]
result.attempts; // 2Confidence deductions:
- JSON not valid first try: -15
- Each retry: -10
- Type coercion per field: -5
- Missing field filled: -8
Schema Definition
import { defineSchema } from "@painitehq/structured-llm";
const schema = defineSchema("person", {
name: { type: "string", description: "Full name" },
age: { type: "number" },
isStudent: { type: "boolean" },
hobbies: { type: "array", items: { type: "string" } },
address: {
type: "object",
properties: {
city: { type: "string" },
zip: { type: "string" },
},
},
});Supported types: string, number, boolean, array, object
API
extract<T>(input, schema, options)
Returns Promise<ExtractionResult<T>>:
interface ExtractionResult<T> {
data: T; // typed structured data
raw: string; // raw LLM response
model: string; // model used
confidence: number; // 0-100 score
repairLog: RepairAction[]; // what was fixed
attempts: number; // how many tries
usage?: {
promptTokens: number;
completionTokens: number;
totalTokens: number;
};
}Options
{
apiKey?: string; // OpenRouter API key (or set OPENROUTER_API_KEY env var)
model?: string; // model to use (default: "openrouter/free")
temperature?: number; // 0-1 (default: 0)
maxRetries?: number; // max retry attempts (default: 3)
timeout?: number; // request timeout in ms (default: 60000)
}Requirements
- OpenRouter API key (get one at https://openrouter.ai)
- Any runtime: Node.js, Bun, Deno, browsers
License
MIT
