Vercel AI SDK vs calling model APIs directly: what you actually gain A developer compared using the Vercel AI SDK versus calling model APIs directly in production Next.js apps. The SDK provides built-in streaming, provider agnosticism, and unified tool call handling, while raw fetch is simpler for one-shot completions. The tradeoff depends on whether the app needs streaming, multi-provider support, or middleware for caching and logging. You can ship a working LLM feature with nothing but fetch and a model's REST endpoint. People do it every day, and for a one shot completion it is honestly fine. So the real question is not "can I call the API directly" but "what am I giving up by not adding the Vercel AI SDK, and is that tradeoff worth a dependency?" I have wired both approaches into production Next.js apps, and the answer is not the blanket "always use the SDK" you see on Twitter. Here is what the abstraction actually buys you, where it earns its footprint, and when reaching for raw fetch is the smarter call. If you are already deep into building AI products with Next.js https://mudassirkhan.me/services/nextjs-for-ai-products , some of this will be familiar, but the middleware section is where most people leave value on the table. Here is a bare completion against a chat endpoint. No SDK, no wrapper, just the request. js async function ask prompt: string { const res = await fetch "https://api.openai.com/v1/chat/completions", { method: "POST", headers: { "Content-Type": "application/json", Authorization: Bearer ${process.env.OPENAI API KEY} , }, body: JSON.stringify { model: "gpt-4o", messages: { role: "user", content: prompt } , } , } ; const data = await res.json ; return data.choices 0 .message.content; } That's the whole thing. If your feature is a single server side completion a summarizer, a classifier, a one shot rewrite this is clean and you should not feel bad about it. No bundle cost, no new API to learn, and you can read every byte on the wire. The pain starts the moment you want any of these: streaming tokens to the browser, swapping to a different provider, handling tool calls, or attaching caching and logging without touching your feature code. Each of those is solvable by hand. The question is whether you want to solve them by hand every time. The same completion, streamed, through the SDK: js import { streamText } from "ai"; import { openai } from "@ai-sdk/openai"; export async function POST req: Request { const { prompt } = await req.json ; const result = streamText { model: openai "gpt-4o" , prompt, } ; return result.toDataStreamResponse ; } Three things happened here that are annoying to do with raw fetch . First, streaming. streamText gives you a properly framed data stream that the client hook consumes with no manual parsing of server sent event chunks. If you have ever hand parsed data: lines out of a readable stream, you know why this matters. Second, provider agnosticism. Swapping OpenAI for Anthropic is a single line: php import { anthropic } from "@ai-sdk/anthropic"; // model: openai "gpt-4o" - model: anthropic "claude-sonnet-4" , The unified API spans OpenAI, Anthropic, Google, Mistral, and dozens of other providers, so the rest of your code does not change. With raw calls, each provider has its own request shape, its own streaming format, and its own tool call schema, and you end up writing an adapter layer yourself. That adapter layer is basically the SDK, except untested and maintained by you. Third, tool calls. Streaming partial tool calls as part of the data stream is now stable, which means the model can call your functions and you can render the arguments as they arrive instead of waiting for the full response. Doing this by hand against multiple providers is where the footgun count really climbs. Here is the honest comparison: | Concern | Raw fetch | Vercel AI SDK | |---|---|---| | One shot completion | Trivial | Trivial slight overhead | | Token streaming to browser | Manual SSE parsing | Built in | | Switch provider | Rewrite request + parsing | One line | | Tool calls across providers | Per provider schema | Unified | | Caching / logging / guardrails | Wrap every call site | Middleware, one place | | Bundle footprint | Zero | Real, but modest | This is the part people underrate. Middleware lets you intercept and modify model calls without changing a single line of application code. Guardrails, caching, and logging all live in one wrapper instead of being smeared across every call site. js import { wrapLanguageModel } from "ai"; import { openai } from "@ai-sdk/openai"; const cachingMiddleware = { wrapGenerate: async { doGenerate, params } = { const key = hash params ; const hit = await cache.get key ; if hit return hit; const result = await doGenerate ; await cache.set key, result ; return result; }, }; const model = wrapLanguageModel { model: openai "gpt-4o" , middleware: cachingMiddleware, } ; Now every call through model is cached, and your route handlers do not know or care. Swap the middleware for a logging wrapper and you get full request tracing with zero call site edits. Chain several and you have guardrails plus caching plus logging composed cleanly. The caching case connects directly to your bill. Repeated identical prompts are pure waste, and a cache in front of the model is one of the highest leverage moves for LLM inference cost https://mudassirkhan.me/blog/llm-inference-cost-optimization . With raw fetch you would bolt this onto each call site and hope nobody adds a new one that forgets the cache. Middleware makes the wrapper the only path in. On the frontend, useChat handles the loop you would otherwise build by hand: appending messages, streaming the assistant reply token by token, tracking loading and error state. js "use client"; import { useChat } from "ai/react"; export function Chat { const { messages, input, handleInputChange, handleSubmit } = useChat ; return