fern/01-guide/09-comparisons/ai-sdk.mdx
AI SDK by Vercel is a powerful toolkit for building AI-powered applications in TypeScript. It's particularly popular for Next.js and React developers.
Let's explore how AI SDK handles structured extraction and where the complexity creeps in.
AI SDK makes structured data generation look elegant at first:
import { generateObject } from 'ai';
import { openai } from '@ai-sdk/openai';
import { z } from 'zod';
const Resume = z.object({
name: z.string(),
skills: z.array(z.string())
});
const { object } = await generateObject({
model: openai('gpt-4o'),
schema: Resume,
prompt: 'John Doe, Python, Rust'
});
Clean and simple! But let's make it more realistic by adding education:
+const Education = z.object({
+ school: z.string(),
+ degree: z.string(),
+ year: z.number()
+});
const Resume = z.object({
name: z.string(),
skills: z.array(z.string()),
+ education: z.array(Education)
});
const { object } = await generateObject({
model: openai('gpt-4o'),
schema: Resume,
prompt: `John Doe
Python, Rust
University of California, Berkeley, B.S. in Computer Science, 2020`
});
Still works! But... what's the actual prompt being sent? How many tokens is this costing?
Your manager asks: "Why did the extraction fail for this particular resume?"
// How do you debug what went wrong?
const { object } = await generateObject({
model: openai('gpt-4o'),
schema: Resume,
prompt: complexResumeText
});
// You can't see:
// - The actual prompt sent to the model
// - The schema format used
// - Why certain fields were missed
You start digging through the AI SDK source code to understand the prompt construction...
Now your PM wants to classify resumes by seniority level:
const SeniorityLevel = z.enum(['junior', 'mid', 'senior', 'staff']);
const Resume = z.object({
name: z.string(),
skills: z.array(z.string()),
education: z.array(Education),
seniority: SeniorityLevel
});
But wait... how do you tell the model what "junior" vs "senior" means? Zod enums are just string literals:
// You can't add descriptions to enum values!
// How does the model know junior = 0-2 years experience?
// You try adding a comment...
const SeniorityLevel = z.enum([
'junior', // 0-2 years
'mid', // 2-5 years
'senior', // 5-10 years
'staff' // 10+ years
]);
// But comments aren't sent to the model!
// So you end up doing this hack:
const { object } = await generateObject({
model: openai('gpt-4o'),
schema: Resume,
prompt: `Extract resume information.
Seniority levels:
- junior: 0-2 years experience
- mid: 2-5 years experience
- senior: 5-10 years experience
- staff: 10+ years experience
Resume:
${resumeText}`
});
Your clean abstraction is leaking...
Your company wants to use different models for different use cases:
// First, install a bunch of packages
npm install @ai-sdk/openai @ai-sdk/anthropic @ai-sdk/google @ai-sdk/mistral
// Import from different packages
import { openai } from '@ai-sdk/openai';
import { anthropic } from '@ai-sdk/anthropic';
import { google } from '@ai-sdk/google';
// Now you need provider detection logic
function getModel(provider: string) {
switch(provider) {
case 'openai': return openai('gpt-4o');
case 'anthropic': return anthropic('claude-3-opus-20240229');
case 'google': return google('gemini-pro');
// Don't forget to handle errors...
}
}
// And manage different API keys
const providers = {
openai: process.env.OPENAI_API_KEY,
anthropic: process.env.ANTHROPIC_API_KEY,
google: process.env.GOOGLE_API_KEY,
// More environment variables to manage...
};
You want to test your extraction logic:
// How do you test this without API calls?
const { object } = await generateObject({
model: openai('gpt-4o'),
schema: Resume,
prompt: testResumeText
});
// Mock the entire AI SDK?
jest.mock('ai', () => ({
generateObject: jest.fn().mockResolvedValue({
object: { name: 'Test', skills: ['JS'] }
})
}));
// But you're not testing your schema or prompt...
// Just that your mocks return the right shape
As your app grows, you need:
Your code evolves into:
class ResumeExtractor {
private tokenCounter: TokenCounter;
private promptTemplates: Map<string, string>;
private retryConfig: RetryConfig;
async extract(text: string, options?: ExtractOptions) {
const model = this.selectModel(options);
const prompt = this.buildPrompt(text, options);
return this.withRetry(async () => {
const start = Date.now();
const tokens = this.tokenCounter.estimate(prompt);
try {
const result = await generateObject({
model,
schema: Resume,
prompt
});
this.logUsage({ tokens, duration: Date.now() - start });
return result;
} catch (error) {
this.handleError(error);
}
});
}
// ... dozens more methods
}
The simple AI SDK call is now buried in layers of infrastructure code.
BAML was designed for the reality of production LLM applications. Here's the same resume extraction:
class Education {
school string
degree string
year int
}
enum SeniorityLevel {
JUNIOR @description("0-2 years of experience")
MID @description("2-5 years of experience")
SENIOR @description("5-10 years of experience")
STAFF @description("10+ years of experience, technical leadership")
}
class Resume {
name string
skills string[]
education Education[]
seniority SeniorityLevel
}
function ExtractResume(resume_text: string) -> Resume {
client GPT4
prompt #"
Extract the following information from the resume.
Resume:
---
{{ resume_text }}
---
{{ ctx.output_format }}
"#
}
Notice what you get immediately:
// All providers in one place
client<llm> GPT4 {
provider openai
options {
model "gpt-4o"
temperature 0.1
}
}
client<llm> Claude {
provider anthropic
options {
model "claude-3-opus-20240229"
temperature 0.1
}
}
client<llm> Gemini {
provider google
options {
model "gemini-pro"
}
}
client<llm> Llama {
provider ollama
options {
model "llama3"
}
}
// Same function, any model
function ExtractResume(resume_text: string) -> Resume {
client GPT4 // Just change this
prompt #"..."#
}
Use it in TypeScript:
import { b } from '@/baml_client';
// Use default model
const resume = await b.ExtractResume(resumeText);
// Switch models based on your needs
const complexResume = await b.ExtractResume(complexText, { client: "Claude" });
const simpleResume = await b.ExtractResume(simpleText, { client: "Llama" });
// Everything is fully typed!
console.log(resume.seniority); // TypeScript knows this is SeniorityLevel
With BAML's VSCode extension, you can:
No mocking required - you're testing the actual prompt and parsing logic.
AI SDK is fantastic for building streaming AI applications in Next.js. But for structured extraction, you end up fighting the abstractions.
BAML's advantages over AI SDK:
What this means for your TypeScript apps:
AI SDK is great for: Rapid prototyping, simple use cases BAML is great for: Production structured extraction, multi-model apps, cost optimization, streaming UIs with semantic streaming
We built BAML because we were tired of elegant APIs that fall apart when you need production reliability and control.
BAML does have some limitations:
Ready for bulletproof structured extraction with full control? Try BAML.