I argued last week that an AI SDK call with no bounds is three CWEs in one missing config object. Fair question back: does anyone actually ship that?
So I linted for it. 20,004 source files across 14 public repositories, 474 of which import the Vercel AI SDK, 116 of which contain a real generation call. That 116 is the denominator; everything below is a fraction of it.
| Bound | Files missing it | Repositories |
|---|---|---|
output cap (maxOutputTokens) |
103 / 116 | 12 of 14 |
| request timeout | 50 / 116 | 9 of 14 |
| abort signal on a stream | 44 / 116 | 7 of 14 |
Read the repo column, not the percentage
88.8% is the number that would travel, and it is the one I trust least. One repository, cloudflare_agents, contributes 52 of those 103 files — just over half. Percentages of files measure how a codebase is split into modules as much as how it was written.
The repo count is the robust statistic. Twelve of fourteen survives deleting any single project from the corpus. The percentage does not.
Five of the fourteen are Vercel's own
ai-elements, vercel_chat, vercel_examples, vercel_streamdown and vercel-labs_agent-eval are the SDK vendor's repositories, and they are in the findings.
That is not a gotcha — it is the mechanism. Example code is deliberately minimal; a ceiling is noise in a snippet teaching you streamText. But the minimal shape is the shape that gets copied, and it is missing the same parameter in the reader's production handler, where it is no longer noise. The omission is correct upstream and wrong downstream, which is exactly why nothing catches it.
What an unbounded call looks like in the wild
Not exotic. This is the common form:
const result = streamText({ model, messages });
The one worth showing is subtler — 302ai_302-AI-Studio builds its options indirectly:
const streamTextOptions = { ...baseConfig, ...(prompt && { system: prompt }) };
const result = await generateText(streamTextOptions);
I assumed this was a false positive: a rule cannot see into an object built elsewhere. It isn't. baseConfig sets model, messages, providerOptions and tools — and the whole 1,500-line file contains exactly one occurrence of timeout, abortSignal or maxOutputTokens between them.
The bound I threw out
The audit measured a fourth — step count — and I am not reporting it. Two reasons, either sufficient. The SDK already defaults stopWhen to stepCountIs(1), so the loop is bounded unless you raised it yourself; a rule firing there reports a non-defect. And spot-checking four of its flagged files found one that sets both stopWhen and stepCountIs and was flagged anyway.
A rule that fires on a safe default doesn't make the number bigger. It makes the other three unbelievable.
Run it on yours
// eslint.config.mjs — after `npm i -D eslint-plugin-vercel-ai-security`
import ai from "eslint-plugin-vercel-ai-security";
export default [
{
files: ["**/*.ts"],
plugins: { "vercel-ai-security": ai },
rules: { "vercel-ai-security/require-max-tokens": "error" },
},
];
Honest limits, because n = 14 is small — and a corpus you assembled yourself is the hard half of any audit. This is an adoption-scan convenience sample, not a random draw from npm, and two directories in it turned out to be the same project cloned twice — caught by content fingerprint, since every checkout reported the same git remote. The claim is that this is what the SDK's own ecosystem looks like. It is not a population rate.
The prior sweep I tried to reuse recorded findings but no denominator and no repo list, so it could not answer this at all. A count without its denominator isn't a small result. It's not a result.
More of these — follow on dev.to.
Check the handler you shipped this week: is there a ceiling on it, or did the example not have one either?
Related:
Top comments (2)
Dropping the step-count rule because it fired on a safe default is the part that makes the other three numbers believable.
On the repo count: 12 of 14 sounds firmer than it is. A 95% Wilson interval for 12/14 runs from roughly 60% to 96%, and that is before the sampling issue, since you picked the 14 repos rather than drawing them. What the interval can honestly support is "most of the public repos I looked at", not a rate for AI SDK users in general. The cloudflare_agents point you made applies here too: repos with one maintainer style behave as a single draw, so the effective n is probably lower than 14.
A check that would sharpen it: split the 103 files by whether the call sits in an example or docs directory versus an application route handler. You argued the omission is correct upstream and wrong downstream. If the app-route files miss the cap at a similar rate to the examples, that is the more worrying result, and it is a number the 116 denominator already contains.
repo count over percent is the right read. did any of the 14 set a cap in the client but skip it on the server route, or was it always gone in both?