TypeScript AI SDK Comparison: Vercel AI SDK vs OpenAI Agents SDK for Agent Development
A practical comparison of TypeScript AI SDKs for building agents: Vercel AI SDK, OpenAI Agents SDK, and AWS Bedrock, with code examples and decision frameworks.
Building an AI agent in TypeScript starts with a boring decision: which SDK carries your tool calls, your streaming, and your provider credentials. Three answers dominate, and each optimizes for something different. Vercel AI SDK trades provider-specific surface for portability, OpenAI Agents SDK builds multi-agent orchestration into the runtime, and direct provider SDKs keep every knob exposed.
For most TypeScript and Next.js codebases the default is Vercel AI SDK. The interesting question is where that default stops paying: heavy multi-agent orchestration, and single-provider services that need a feature no abstraction exposes yet.
The TypeScript AI SDK Landscape
Each of the three places the abstraction at a different level:
- Vercel AI SDK: Provider-agnostic unified interface with 70+ provider support
- OpenAI Agents SDK: Purpose-built for multi-agent systems with native handoffs
- Direct Provider SDKs: Maximum control with provider-specific features
The challenge is matching your requirements to the right level.
Vercel AI SDK: The Provider-Agnostic Approach
Vercel AI SDK takes a unified interface approach. Write once, deploy to any provider. This flexibility matters when requirements change or when you need fallback providers for reliability.
Core Architecture
The SDK separates concerns cleanly:
- AI SDK Core: Server-side operations (
generateText,streamText,generateObject) - AI SDK UI: React hooks for chat interfaces (
useChat,useCompletion) - AI SDK RSC: React Server Components integration
Tool Definition with Zod
Tools are defined with type-safe Zod schemas. The SDK handles parameter validation automatically:
import { tool, generateText } from 'ai';
import { openai } from '@ai-sdk/openai';
import { z } from 'zod';
const weatherTool = tool({
description: 'Get current weather for a city',
parameters: z.object({
city: z.string().describe('City name'),
unit: z.enum(['celsius', 'fahrenheit']).default('celsius'),
}),
execute: async ({ city, unit }) => {
// Your API call here
const response = await fetch(
`https://api.weather.example/v1/current?city=${city}&unit=${unit}`
);
return response.json();
},
});
const searchTool = tool({
description: 'Search the web for information',
parameters: z.object({
query: z.string().describe('Search query'),
limit: z.number().optional().default(5),
}),
execute: async ({ query, limit }) => {
// Search implementation
return { results: [`Result for: ${query}`], count: limit };
},
});
Agent Loop with maxSteps
For multi-turn tool usage, the maxSteps parameter enables automatic tool execution loops:
import { streamText } from 'ai';
import { openai } from '@ai-sdk/openai';
export async function POST(req: Request) {
const { messages } = await req.json();
const result = streamText({
model: openai('gpt-4o'),
messages,
system: 'You are a helpful assistant with weather and search capabilities.',
tools: {
weather: weatherTool,
search: searchTool,
},
maxSteps: 5, // Allow up to 5 tool execution rounds
});
return result.toDataStreamResponse();
}
The SDK handles the entire loop: call LLM, detect tool calls, execute tools, append results, repeat until complete or maxSteps reached.
Provider Switching Pattern
Provider switching is where the unified interface pays off. Same code, different backend:
import { openai } from '@ai-sdk/openai';
import { anthropic } from '@ai-sdk/anthropic';
import { google } from '@ai-sdk/google';
import { createAmazonBedrock } from '@ai-sdk/amazon-bedrock';
import { generateText } from 'ai';
// Configure providers
const bedrock = createAmazonBedrock({ region: 'us-east-1' });
// Provider registry
const providers = {
'gpt-4o': openai('gpt-4o'),
'gpt-4o-mini': openai('gpt-4o-mini'),
'claude-sonnet': anthropic('claude-sonnet-4-6-20250217'),
'claude-haiku': anthropic('claude-haiku-4-5-20241022'),
'gemini-flash': google('gemini-2.5-flash'),
'bedrock-claude': bedrock('anthropic.claude-sonnet-4-6-20250217-v1:0'),
};
// Same function works with any provider
async function generate(prompt: string, providerId: keyof typeof providers) {
const { text, usage } = await generateText({
model: providers[providerId],
prompt,
});
return { text, usage };
}
// Switching is trivial
const openaiResult = await generate('Explain quantum computing', 'gpt-4o');
const claudeResult = await generate('Explain quantum computing', 'claude-sonnet');
Streaming with React Integration
AI SDK UI provides hooks that handle streaming complexity:
// app/api/chat/route.ts
import { openai } from '@ai-sdk/openai';
import { streamText } from 'ai';
export const runtime = 'edge';
export async function POST(req: Request) {
const { messages } = await req.json();
const result = streamText({
model: openai('gpt-4o'),
messages,
tools: {
calculate: tool({
description: 'Perform arithmetic',
parameters: z.object({ expression: z.string() }),
execute: async ({ expression }) => {
// Use a safe math parser in production
return { result: eval(expression) };
},
}),
},
maxSteps: 3,
});
return result.toDataStreamResponse();
}
// components/Chat.tsx
'use client';
import { useChat } from 'ai/react';
export function Chat() {
const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat({
api: '/api/chat',
});
return (
<div className="flex flex-col h-screen">
<div className="flex-1 overflow-y-auto p-4">
{messages.map((m) => (
<div key={m.id} className={`mb-4 ${m.role === 'user' ? 'text-right' : ''}`}>
<span className="font-bold">{m.role}:</span> {m.content}
</div>
))}
</div>
<form onSubmit={handleSubmit} className="p-4 border-t">
<input
value={input}
onChange={handleInputChange}
disabled={isLoading}
className="w-full p-2 border rounded"
placeholder="Type a message..."
/>
</form>
</div>
);
}
OpenAI Agents SDK: Multi-Agent Specialist
OpenAI’s Agents SDK takes a different approach. Rather than provider abstraction, it focuses on agent orchestration patterns: handoffs between specialized agents, guardrails for validation, and built-in tracing.
Core Primitives
The SDK introduces four key concepts:
- Agents: LLMs with instructions, tools, and handoff capability
- Handoffs: Specialized tool calls that transfer conversation ownership
- Guardrails: Input/output validation running in parallel with agent execution
- Tracing: Built-in debugging and monitoring
Multi-Agent with Handoffs
The handoff pattern enables specialist agents that delegate to each other:
import { Agent, run, tool } from '@openai/agents';
import { z } from 'zod';
// Define specialist tools
const getWeatherTool = tool({
name: 'get_weather',
description: 'Get weather for a city',
parameters: z.object({
city: z.string(),
}),
execute: async ({ city }) => {
return `Weather in ${city}: 22C, sunny`;
},
});
const searchDatabaseTool = tool({
name: 'search_database',
description: 'Search internal database',
parameters: z.object({
query: z.string(),
}),
execute: async ({ query }) => {
return `Found 3 results for: ${query}`;
},
});
// Create specialist agents
const weatherAgent = new Agent({
name: 'Weather Specialist',
instructions: 'You are a weather expert. Provide detailed weather information.',
tools: [getWeatherTool],
handoffDescription: 'Specialist for weather-related questions',
});
const dataAgent = new Agent({
name: 'Data Specialist',
instructions: 'You are a data expert. Search and analyze database information.',
tools: [searchDatabaseTool],
handoffDescription: 'Specialist for database queries and data analysis',
});
// Create triage agent with handoffs
const triageAgent = new Agent({
name: 'Triage Agent',
instructions: `You are a helpful assistant that routes questions to specialists.
- For weather questions, hand off to Weather Specialist
- For data/database questions, hand off to Data Specialist
- For general questions, answer directly`,
handoffs: [weatherAgent, dataAgent],
});
// Execute agent workflow
async function handleQuery(userMessage: string) {
const result = await run(triageAgent, userMessage);
return {
finalOutput: result.finalOutput,
agentPath: result.history
.filter(h => h.type === 'handoff')
.map(h => h.agent),
};
}
Agent Loop Execution
The SDK manages a sophisticated execution loop:
Complex Tool Schemas
The SDK handles nested schemas with automatic validation:
const createOrderTool = tool({
name: 'create_order',
description: 'Create a new customer order',
parameters: z.object({
customerId: z.string().uuid(),
items: z.array(z.object({
productId: z.string(),
quantity: z.number().int().positive(),
price: z.number().positive(),
})),
shippingAddress: z.object({
street: z.string(),
city: z.string(),
country: z.string(),
postalCode: z.string(),
}),
priority: z.enum(['standard', 'express', 'overnight']).default('standard'),
}),
execute: async ({ customerId, items, shippingAddress, priority }) => {
const order = await orderService.create({
customerId,
items,
shippingAddress,
priority,
});
return {
orderId: order.id,
status: 'created',
estimatedDelivery: order.estimatedDelivery,
};
},
});
AWS Bedrock Integration
For teams invested in AWS infrastructure, Bedrock provides access to multiple foundation models with enterprise features like IAM, VPC integration, and compliance controls.
AI SDK with Bedrock Provider
The cleanest approach uses AI SDK’s Bedrock provider:
import { createAmazonBedrock } from '@ai-sdk/amazon-bedrock';
import { generateText, streamText } from 'ai';
const bedrock = createAmazonBedrock({
region: 'us-east-1',
// Uses AWS credential chain by default
});
// Claude via Bedrock
const claudeModel = bedrock('anthropic.claude-sonnet-4-6-20250217-v1:0');
// Llama via Bedrock
const llamaModel = bedrock('meta.llama3-70b-instruct-v1:0');
// Amazon Nova (use cross-region inference ID for multi-region availability)
const novaModel = bedrock('amazon.nova-pro-v1:0');
// Alternative: bedrock('us.amazon.nova-pro-v1:0') for cross-region inference
async function generateWithBedrock(prompt: string) {
const { text, usage } = await generateText({
model: claudeModel,
prompt,
maxTokens: 1024,
});
return { text, usage };
}
Lambda Integration
Bedrock works naturally with Lambda using IAM role credentials:
import { createAmazonBedrock } from '@ai-sdk/amazon-bedrock';
import { fromNodeProviderChain } from '@aws-sdk/credential-providers';
import { generateText } from 'ai';
import type { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
const bedrock = createAmazonBedrock({
region: process.env.AWS_REGION || 'us-east-1',
credentialProvider: fromNodeProviderChain(),
});
export const handler = async (
event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> => {
const { prompt } = JSON.parse(event.body || '{}');
const { text } = await generateText({
model: bedrock('anthropic.claude-sonnet-4-6-20250217-v1:0'),
prompt,
});
return {
statusCode: 200,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ response: text }),
};
};
Practical Comparison
Feature Matrix
| Feature | Vercel AI SDK | OpenAI Agents SDK | Direct SDKs |
|---|---|---|---|
| Multi-Provider | 70+ providers | Adapters needed | Single |
| Tool Calling | First-class | First-class | Provider-specific |
| Streaming | Built-in | Built-in | Provider-specific |
| Multi-Agent | Via composition | Native handoffs | Manual |
| Edge Runtime | Full support | Partial | Varies |
| React Integration | Native hooks | Manual | Manual |
| Type Safety | Full TypeScript | Full TypeScript | Varies |
| Observability | DevTools + OTEL | Built-in tracing | Manual |
Setup Effort
Relative effort to reach a first working implementation:
| Task | AI SDK | OpenAI Agents | Direct SDK |
|---|---|---|---|
| Basic chat | Low | Low | Medium |
| Streaming UI | Low | Medium | High |
| Tool calling | Low | Low | Medium |
| Multi-agent | Medium | Low | High |
| Provider switch | Low | Medium | High |
The last row is the one that surfaces later in a project’s life. On a unified API a provider switch is a registry edit; on direct SDKs it means rewriting the call layer for every provider you add.
Cost Considerations
All SDKs are free. Costs come from API usage:
| Model | Provider | Input (per 1M) | Output (per 1M) |
|---|---|---|---|
| GPT-4o | OpenAI | $2.50 | $10.00 |
| GPT-4o-mini | OpenAI | $0.15 | $0.60 |
| Claude Sonnet 4.6 | Anthropic/Bedrock | $3.00 | $15.00 |
| Claude Haiku 4.5 | Anthropic/Bedrock | $1.00 | $5.00 |
| Llama 3.3 70B | Bedrock | $0.72 | $0.72 |
Decision Framework
Choosing the right SDK depends on your specific requirements:
Choose Vercel AI SDK When
- Building with Next.js or React
- Need to support multiple AI providers
- Want streaming UI out of the box
- Value type-safe, unified API
- Need edge runtime compatibility
- Building products that may switch providers
Choose OpenAI Agents SDK When
- Building complex multi-agent systems
- Need native handoff patterns
- Want built-in guardrails
- Prefer explicit tracing and debugging
- Primarily using OpenAI models
- Coming from Python agent frameworks
Choose Direct SDKs When
- Need provider-specific features
- Maximum performance is critical
- Simple use case with single provider
- Want minimal dependencies
- Building SDK or library for others
Choose Bedrock with AI SDK When
- AWS-native infrastructure
- Need enterprise security (VPC, IAM)
- Want Claude without direct Anthropic billing
- Building for regulated industries
- Need model diversity in one platform
Production Patterns
Tiered Model Routing
Match model capability to query complexity:
const modelTiers = {
simple: openai('gpt-4o-mini'),
standard: openai('gpt-4o'),
complex: anthropic('claude-sonnet-4-6-20250217'),
};
function classifyComplexity(input: string): keyof typeof modelTiers {
if (input.length < 50 && !input.includes('analyze')) return 'simple';
if (input.includes('compare') || input.includes('design')) return 'complex';
return 'standard';
}
async function smartGenerate(input: string) {
const tier = classifyComplexity(input);
return generateText({ model: modelTiers[tier], prompt: input });
}
How much this saves depends entirely on your traffic mix. Measure the share of short, lookup-style queries before you assume a number.
Fallback Chain
For high availability, chain multiple providers:
const providerChain = [
openai('gpt-4o'),
anthropic('claude-sonnet-4-6-20250217'),
bedrock('anthropic.claude-sonnet-4-5-20250929-v1:0'),
];
async function generateWithFallback(prompt: string) {
for (const model of providerChain) {
try {
return await generateText({ model, prompt });
} catch (error) {
console.log(`Provider failed, trying next: ${error.message}`);
continue;
}
}
throw new Error('All providers failed');
}
Observability Setup
Track critical metrics in production:
import { trace, SpanStatusCode } from '@opentelemetry/api';
const tracer = trace.getTracer('ai-agent');
async function generateWithTracing(prompt: string) {
return tracer.startActiveSpan('ai.generate', async (span) => {
try {
span.setAttributes({
'ai.model': 'gpt-4o',
'ai.prompt.length': prompt.length,
});
const { text, usage } = await generateText({
model: openai('gpt-4o'),
prompt,
});
span.setAttributes({
'ai.completion.tokens': usage.completionTokens,
'ai.prompt.tokens': usage.promptTokens,
'ai.total.tokens': usage.totalTokens,
});
span.setStatus({ code: SpanStatusCode.OK });
return { text, usage };
} catch (error) {
span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
throw error;
} finally {
span.end();
}
});
}
Common Pitfalls
Unbounded Agent Loops
Without step limits, agents can run indefinitely:
// Problem: No boundaries
const result = streamText({
model: openai('gpt-4'),
tools: myTools,
// No maxSteps - can loop forever
});
// Solution: Always set limits
const result = streamText({
model: openai('gpt-4'),
tools: myTools,
maxSteps: 10, // Explicit boundary
});
Blocking Streams
Waiting for complete responses defeats streaming benefits:
// Problem: Blocks until complete
const result = await streamText({ model, prompt });
const fullText = await result.text;
return new Response(fullText);
// Solution: Pass through stream
const result = streamText({ model, prompt });
return result.toDataStreamResponse();
Ignoring Context Limits
Large conversation histories exceed context windows:
// Problem: Unbounded context
const messages = entireConversationHistory;
await generateText({ model, messages });
// Solution: Manage context actively
const maxTokens = 100000;
const trimmedMessages = trimToFitContext(messages, maxTokens);
await generateText({ model, messages: trimmedMessages });
Where the Default Holds
The Vercel AI SDK default holds as long as your agent is essentially one loop with tools attached, which covers most product work. The provider registry, the streaming transport, and the React hooks ship together, so a provider change stays a registry edit.
Override it in two situations. When several specialist agents need to hand a conversation to each other, OpenAI Agents SDK gives you handoffs, guardrails, and tracing instead of hand-rolled routing. When you are pinned to one provider and need a capability no abstraction exposes yet, that provider’s own SDK costs less than fighting a wrapper.
Either way, generateText() with two tools is enough to prove the shape before you commit to an agent framework.
References
- Vercel AI SDK Documentation - Official AI SDK docs: Core, UI, RSC, providers, and streaming
- Vercel AI SDK: Agents - Agent loop patterns, tool calling, and multi-step orchestration
- OpenAI Agents SDK - OpenAI’s SDK for multi-agent systems with handoffs and guardrails
- Anthropic Client SDKs - Official TypeScript SDK for Claude API integration
- Vercel AI SDK: Tool Calling - Defining and executing tools in AI SDK agents
Related posts
Discover how Middy transforms Lambda development with middleware patterns, moving from repetitive boilerplate to clean, maintainable serverless functions
Implement secure cross-account event distribution with Amazon SNS and SQS: IAM policies, KMS encryption, AWS CDK, and common production pitfalls.
Build maintainable, type-safe Lambda middleware with Middy's builder pattern, Zod validation, feature flags, and secrets management for serverless apps.
Set up a production-grade link shortener with AWS CDK, DynamoDB, and Lambda: architecture decisions, initial setup, and lessons from URL shorteners at scale.
AppSync subscriptions fire only on mutations. This explores bridging downstream BFF events into a NONE-data-source mutation with EventBridge and CDK.