ai-contracts
Reliability layer for AI outputs: schema validation, JSON repair, retry, fallback, and budget guards.
Stop losing users to broken JSON and flaky LLM responses. ai-contracts gives you production-grade structured output handling in a few lines of code.
30-Second Quickstart
npm install ai-contracts zod
import { safeParse } from 'ai-contracts';
import { z } from 'zod';
const UserSchema = z.object({
name: z.string(),
age: z.number(),
});
const result = safeParse(llmOutput, { schema: UserSchema });
if (result.success) {
console.log(result.data);
} else {
console.error(result.error.code);
}
Why ai-contracts?
Real problems this solves:
- 🔧 Malformed JSON - LLMs return markdown code blocks, trailing commas, unquoted keys
- 📋 Schema mismatches - Response structure doesn't match your types
- 🔄 Retry storms - Retries without backoff hammer rate limits
- 🔀 Unstable fallbacks - Switching providers breaks your app
- 💸 Budget blowouts - No visibility into token/cost usage
- 🔍 Debugging nightmares - Raw output lost when parsing fails
Features
| Schema Validation | Zod-first, type-safe validation |
| JSON Repair | Auto-fix markdown blocks, trailing commas, unquoted keys |
| Bounded Retry | Exponential backoff with jitter |
| Ordered Fallback | Deterministic provider failover |
| Budget Guards | Token and cost limits with hard stops |
| Typed Errors | Error codes, raw output traces, provider metadata |
| Observability Hooks | onAttempt, onRepair, onRetry, onFallback, onFail |
Installation
npm install ai-contracts zod
Core API
safeParse(input, options)
Parse and validate a string against a Zod schema.
import { safeParse } from 'ai-contracts';
import { z } from 'zod';
const schema = z.object({ answer: z.string() });
const result = safeParse('```json\n{answer: "hello"}\n```', { schema });
safeGenerate(generateFn, callOptions, options)
Full pipeline: generate → parse → validate → retry → fallback.
import { safeGenerate } from 'ai-contracts';
import { createOpenAIGenerateFn } from 'ai-contracts/adapters/openai';
import OpenAI from 'openai';
import { z } from 'zod';
const openai = new OpenAI();
const generate = createOpenAIGenerateFn(openai);
const result = await safeGenerate(
generate,
{
prompt: 'Return a JSON object with name and age',
model: 'gpt-4o',
},
{
schema: z.object({ name: z.string(), age: z.number() }),
retry: { maxAttempts: 3 },
fallbacks: [
{ provider: 'openai', model: 'gpt-4o-mini' },
],
budget: { maxTokens: 1000 },
hooks: {
onRetry: ({ attempt, error }) => console.log(`Retry ${attempt}: ${error.message}`),
onFallback: ({ from, to }) => console.log(`Fallback: ${from.model} → ${to.model}`),
},
}
);
withRetry(fn, policy, hooks)
Retry any async function with configurable backoff.
import { withRetry } from 'ai-contracts';
const data = await withRetry(
() => fetchFromAPI(),
{
maxAttempts: 3,
baseDelayMs: 1000,
exponentialBackoff: true,
}
);
withFallback(fn, fallbacks, createFallbackFn)
Execute with ordered fallback providers.
import { withFallback } from 'ai-contracts';
const result = await withFallback(
() => primaryProvider(),
[{ provider: 'backup', model: 'backup-model' }],
(config) => () => backupProvider(config)
);
withBudget(fn, config)
Enforce token and cost limits.
import { withBudget } from 'ai-contracts';
const result = await withBudget(
async () => ({
result: await generate(),
usage: { promptTokens: 100, completionTokens: 50, totalTokens: 150 },
}),
{ maxTokens: 1000, maxCostUsd: 0.10 }
);
Provider Adapters
OpenAI
import OpenAI from 'openai';
import { createOpenAIGenerateFn } from 'ai-contracts/adapters/openai';
import { safeGenerate } from 'ai-contracts';
const openai = new OpenAI();
const generate = createOpenAIGenerateFn(openai);
const result = await safeGenerate(
generate,
{ prompt: 'Hello', model: 'gpt-4o' },
{ schema: mySchema }
);
Anthropic
import Anthropic from '@anthropic-ai/sdk';
import { createAnthropicGenerateFn } from 'ai-contracts/adapters/anthropic';
import { safeGenerate } from 'ai-contracts';
const anthropic = new Anthropic();
const generate = createAnthropicGenerateFn(anthropic);
const result = await safeGenerate(
generate,
{ prompt: 'Hello', model: 'claude-sonnet-4-20250514' },
{ schema: mySchema }
);
Vercel AI SDK
import { openai } from '@ai-sdk/openai';
import { generateText } from 'ai';
import { createAISDKGenerateFn } from 'ai-contracts/adapters/ai-sdk';
import { safeGenerate } from 'ai-contracts';
const model = openai('gpt-4o');
const generate = createAISDKGenerateFn(generateText, model);
const result = await safeGenerate(
generate,
{ prompt: 'Hello', model: 'gpt-4o' },
{ schema: mySchema }
);
Error Model
All errors include:
interface ContractError {
code: 'PARSE_FAILED' | 'REPAIR_FAILED' | 'VALIDATION_FAILED' |
'RETRY_EXHAUSTED' | 'FALLBACK_EXHAUSTED' | 'BUDGET_EXCEEDED' |
'PROVIDER_ERROR' | 'TIMEOUT';
message: string;
stage: 'strict' | 'repair' | 'validation';
attempts: number;
rawOutput?: string;
meta?: ProviderMeta;
validationErrors?: ZodError['errors'];
cause?: Error;
}
Migration from Manual JSON.parse
Before:
try {
const data = JSON.parse(llmOutput);
} catch (e) {
console.error('Parse failed:', e);
}
After:
import { safeParse } from 'ai-contracts';
import { z } from 'zod';
const result = safeParse(llmOutput, {
schema: z.object({ answer: z.string() }),
});
if (result.success) {
console.log(result.data.answer);
} else {
console.error(result.error.code, result.error.rawOutput);
}
Benchmarks
| Parse success rate | ~85% | ~99% |
| Schema validation errors | Uncaught | Typed & actionable |
| Retry logic | Manual | Built-in with backoff |
| Fallback handling | Ad-hoc | Deterministic chain |
| Debug info on failure | Lost | Full context preserved |
| Overhead | N/A | <1ms parse, <5ms repair |
Tested on 10,000 synthetic malformed JSON samples with common LLM artifacts.
Configuration Reference
Retry Policy
interface RetryPolicy {
maxAttempts: number;
baseDelayMs: number;
exponentialBackoff: boolean;
maxDelayMs: number;
jitter: number;
}
Budget Config
interface BudgetConfig {
maxTokens?: number;
maxCostUsd?: number;
promptTokenCostPer1k?: number;
completionTokenCostPer1k?: number;
}
Logging Hooks
interface LoggingHooks<T> {
onAttempt?: (info: { attempt: number; provider?: string; model?: string }) => void;
onRepair?: (info: { rawOutput: string; repairAttempt: number }) => void;
onRetry?: (info: { attempt: number; error: ContractError; delayMs: number }) => void;
onFallback?: (info: { from: ProviderMeta; to: FallbackConfig; error: ContractError }) => void;
onSuccess?: (result: ContractSuccess<T>) => void;
onFail?: (error: ContractError) => void;
}
TypeScript Support
Full type inference from Zod schemas:
import { safeParse, type InferSchema } from 'ai-contracts';
import { z } from 'zod';
const MySchema = z.object({
name: z.string(),
items: z.array(z.number()),
});
type MyData = InferSchema<typeof MySchema>;
const result = safeParse(input, { schema: MySchema });
if (result.success) {
result.data.name;
}
Contributing
git clone https://github.com/wnxd/ai-contracts.git
cd ai-contracts
npm install
npm run test
npm run typecheck
License
MIT