How can a developer choose one framework from a few similar ones? Popularity is a useful signal - a larger community means more answered questions, more examples, and a lower bus factor. But it doesn't tell you much about how pleasant the framework is to work with.
I decided to battle-test Vercel AI SDK (22.5k ⭐), Mastra (21.9k ⭐), Langchain.js (17.2k ⭐) and Firebase Genkit (5.6k ⭐) by developing a few code examples with them all.
Here I'll show you the examples and share my observations.
ToC
- Example 1: Simple question
- Example 2: Creating a tool
- Example 3: Simple question with a tool call
- More examples
- Conclusions
Example 1: Simple question
Vercel AI SDK
import 'dotenv/config'
import { anthropic } from '@ai-sdk/anthropic'
import { generateText, stepCountIs } from 'ai'
const result = await generateText({
model: anthropic('claude-sonnet-4-6'),
system: "You are Amy, a friendly assistant, who you can chat with about everyday stuff.",
prompt: "What's your name?",
stopWhen: stepCountIs(1),
temperature: 0,
})
console.log(result.text)
// Hi! I'm Amy. Nice to meet you! How are you today?
Mastra
import { Agent } from '@mastra/core/agent'
const agent = new Agent({
name: 'Amy',
instructions: "You are Amy, a friendly assistant, who you can chat with about everyday stuff.",
model: 'anthropic/claude-sonnet-4-6',
})
const result = await agent.generate("What's your name?")
console.log(result.text)
Langchain.js
import 'dotenv/config'
import { ChatAnthropic } from '@langchain/anthropic'
import {
BaseMessage,
HumanMessage,
SystemMessage,
} from '@langchain/core/messages'
const ai = new ChatAnthropic({
model: 'claude-sonnet-4-6',
temperature: 0,
})
const messages: BaseMessage[] = [
new SystemMessage("You are Amy, a friendly assistant, who you can chat with about everyday stuff."),
new HumanMessage("What's your name?"),
]
const response = await ai.invoke(messages)
console.log(response.content)
Firebase Genkit
import 'dotenv/config'
import { genkit } from 'genkit'
import { anthropic, claude45Sonnet } from 'genkitx-anthropic'
const ai = genkit({
plugins: [anthropic({ apiKey: process.env.ANTHROPIC_API_KEY })],
model: claude45Sonnet,
})
const result = await ai.generate({
system: "You are Amy, a friendly assistant, who you can chat with about everyday stuff.",
prompt: "What's your name?",
maxTurns: 1,
config: {
version: 'claude-sonnet-4-6',
temperature: 0,
},
})
console.log(result.text)
Observations
-
Vercel AI SDK and Mastra LLM calls look the cleanest. Vercel is a single function call; Mastra requires instantiating an
Agentobject but keeps the generate call simple. -
Mastra uses a provider-prefixed model string (
'anthropic/claude-sonnet-4-6') for built-in model routing, so no separate provider package is needed. -
I like
SystemMessageandHumanMessageOOP message wrappers in Langchain which are useful for more complex cases. However, for simple cases like the ones above, theprompt/systemfields orinstructionswork better for me. -
Genkit param/return types made me think that they are overly complex.
-
Langchain.js is a port of the Python LangChain library, which was originally launched in October 2022 (Python version) with the JavaScript version following in January 2023. This heritage may explain some of the framework's design choices and verbosity compared to JavaScript-native alternatives.
Example 2: Creating a tool
Vercel AI SDK
import { tool } from 'ai'
import { z } from 'zod'
export const temperatureTool = tool({
description: 'Gets current temperature in the given city',
inputSchema: z.object({
city: z.string().describe('The city to get the current temperature for'),
}),
execute: async ({ city: _city }) => {
try {
const min = -10
const max = 40
const temperature = (Math.random() * (max - min) + min).toFixed(0)
return `${temperature}°C`
} catch (error: any) {
return { error: error.message }
}
},
})
Mastra
import { createTool } from '@mastra/core/tools'
import { z } from 'zod'
export const temperatureTool = createTool({
id: 'temperature',
description: 'Gets current temperature in the given city',
inputSchema: z.object({
city: z.string().describe('The city to get the current temperature for'),
}),
outputSchema: z.object({
temperature: z.string(),
}),
execute: async ({ city: _city }) => {
try {
const min = -10
const max = 40
const temperature = (Math.random() * (max - min) + min).toFixed(0)
return { temperature: `${temperature}°C` }
} catch (error: any) {
return { temperature: error.message }
}
},
})
Langchain.js
import { tool } from '@langchain/core/tools'
import { z } from 'zod'
export const temperatureTool = tool(
async ({ city: _city }) => {
try {
const min = -10
const max = 40
const temperature = (Math.random() * (max - min) + min).toFixed(0)
return `${temperature}°C`
} catch (error: any) {
return error.message
}
},
{
name: 'temperature',
description: 'Gets current temperature in the given city',
schema: z.object({
city: z.string().describe('The city to get the current temperature for'),
}),
responseFormat: 'content',
}
)
Firebase Genkit
import { Genkit, z } from 'genkit'
export const createTemperatureTool = (ai: Genkit) => {
return ai.defineTool(
{
name: 'temperature',
description: 'Gets current temperature in the given city',
inputSchema: z.object({
city: z
.string()
.describe('The city to get the current temperature for'),
}),
outputSchema: z.string(),
},
async ({ city: _city }) => {
try {
const min = -10
const max = 40
const temperature = (Math.random() * (max - min) + min).toFixed(0)
return `${temperature}°C`
} catch (error: any) {
return error.message
}
}
)
}
Observations
-
Unlike Vercel AI SDK and Langchain, Firebase Genkit package exports Zod lib, which means you don't need to install and import it separately.
-
Mastra requires both
inputSchemaandoutputSchema, and its tool returns a typed object rather than a raw string. More verbose, but also more structured. -
Unlike Firebase Genkit and Langchain, Vercel AI SDK doesn't let you set the output format.
-
Unfortunately, Genkit tools require the main Genkit object, so I used a wrapper function to define the tool.
Example 3: Simple question with a tool call
Vercel AI SDK
import 'dotenv/config'
import { anthropic } from '@ai-sdk/anthropic'
import { generateText, stepCountIs } from 'ai'
import { temperatureTool } from './tools/temperature'
const result = await generateText({
model: anthropic('claude-sonnet-4-6'),
system: "You are Amy, a friendly assistant, who you can chat with about everyday stuff.",
prompt: "What's the temperature in New York?",
// Plug the tool here.
tools: { temperature: temperatureTool },
stopWhen: stepCountIs(2),
temperature: 0,
})
console.log(result.text)
Mastra
import { Agent } from '@mastra/core/agent'
import { temperatureTool } from './tools/temperature'
const agent = new Agent({
name: 'Amy',
instructions: "You are Amy, a friendly assistant, who you can chat with about everyday stuff.",
model: 'anthropic/claude-sonnet-4-6',
// Plug the tool here.
tools: { temperature: temperatureTool },
})
const result = await agent.generate("What's the temperature in New York?")
console.log(result.text)
Langchain.js
import 'dotenv/config'
import { ChatAnthropic } from '@langchain/anthropic'
import { BaseMessage, HumanMessage, ToolMessage } from '@langchain/core/messages'
import { createAgent } from 'langchain'
import { temperatureTool } from './tools/temperature'
const ai = new ChatAnthropic({
model: 'claude-sonnet-4-6',
temperature: 0,
})
const agent = createAgent({
model: ai,
// Plug the tool here.
tools: [temperatureTool],
systemPrompt: "You are Amy, a friendly assistant, who you can chat with about everyday stuff.",
})
const messages: BaseMessage[] = [
new HumanMessage("What's the temperature in New York?"),
]
const result = await agent.invoke({
messages,
})
for (let i = messages.length; i < result.messages.length; i++) {
if (
ToolMessage.isInstance(result.messages[i]) ||
typeof result.messages[i].content !== 'string'
) {
continue
}
console.log(result.messages[i].content)
}
Firebase Genkit
import 'dotenv/config'
import { genkit } from 'genkit'
import { anthropic, claude45Sonnet } from 'genkitx-anthropic'
import { createTemperatureTool } from './tools/temperature'
const ai = genkit({
plugins: [anthropic({ apiKey: process.env.ANTHROPIC_API_KEY })],
model: claude45Sonnet,
})
const temperatureTool = createTemperatureTool(ai)
const result = await ai.generate({
// Plug the tool here.
tools: [temperatureTool],
system: "You are Amy, a friendly assistant, who you can chat with about everyday stuff.",
prompt: "What's the temperature in New York?",
maxTurns: 2,
config: {
version: 'claude-sonnet-4-6',
temperature: 0,
},
})
console.log(result.text)
Observations
-
Mastra's tool integration is the cleanest - tools are declared on the
Agentconstructor and the generate call stays the same with or without tools. -
For Vercel AI SDK and Firebase Genkit, I needed to allow at least two iterations for the LLM to be able to call the tool. Mastra and Langchain handle this automatically.
-
For the Langchain.js example, I had to use a
createAgentprimitive from thelangchainpackage because manual tool calling without it was a pain. -
Mastra uses some low-level primitives from Vercel AI SDK under the hood for its LLM integration.
-
Unlike the other frameworks, Genkit doesn't feel fully provider-agnostic - the Claude adapter is third-party and lags behind (no support for Claude 4.6 yet).
-
As you may see, processing of the results for the Langchain.js example is more verbose. Let me know if you know how to improve that.
More examples
You may find an interactive chat with memory and a tool enabled along with other examples in the kometolabs/ai-sdk-comparison repo. Feel free to submit a PR if you know how to improve the examples.
Conclusions
To sum up, I like the functional simplicity and docs of Vercel AI SDK. Mastra is a close second and stands out with its clean agent abstraction and built-in model routing. Langchain requires more boilerplate and result processing, though the createAgent API from the langchain package is a step in the right direction. Firebase Genkit trails in popularity but has a solid plugin-based architecture.
As one wise person said, "healthy competition keeps everybody sharp and gives the community more choice". I don't agree with the "more choice" part, because with more choice, developers need to spend time on learning the differences which they could have spent on developing features for their users. So I'd like to encourage framework builders to collaborate more and compete less.
