Package Exports
- ai-contracts
- ai-contracts/adapters/ai-sdk
- ai-contracts/adapters/anthropic
- ai-contracts/adapters/openai
Readme
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 zodimport { safeParse } from 'ai-contracts';
import { z } from 'zod';
const UserSchema = z.object({
name: z.string(),
age: z.number(),
});
// Parse LLM output safely
const result = safeParse(llmOutput, { schema: UserSchema });
if (result.success) {
console.log(result.data); // { name: "John", age: 30 }
} else {
console.error(result.error.code); // "VALIDATION_FAILED" | "PARSE_FAILED" | etc.
}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
| Feature | Description |
|---|---|
| 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 zodCore 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() });
// Auto-repairs malformed JSON
const result = safeParse('```json\n{answer: "hello"}\n```', { schema });
// result.success === true
// result.data === { answer: "hello" }
// result.stage === "repair"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; // Truncated to 500 chars
meta?: ProviderMeta; // provider, model, requestId
validationErrors?: ZodError['errors'];
cause?: Error;
}Migration from Manual JSON.parse
Before:
// Fragile, no repair, no retries, no observability
try {
const data = JSON.parse(llmOutput);
// Hope it matches your expected shape...
} 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) {
// Type-safe, validated data
console.log(result.data.answer);
} else {
// Actionable error with full context
console.error(result.error.code, result.error.rawOutput);
}Benchmarks
| Metric | Without ai-contracts | With ai-contracts |
|---|---|---|
| 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; // Default: 3
baseDelayMs: number; // Default: 1000
exponentialBackoff: boolean; // Default: true
maxDelayMs: number; // Default: 30000
jitter: number; // Default: 0.1 (10%)
}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>;
// { name: string; items: number[] }
const result = safeParse(input, { schema: MySchema });
if (result.success) {
result.data.name; // TypeScript knows this is string
}Contributing
git clone https://github.com/wnxd/ai-contracts.git
cd ai-contracts
npm install
npm run test
npm run typecheckLicense
MIT