Hallucinated API.
AI-generated code that calls functions, methods, parameters, config keys, or packages that look plausible but do not exist in the actual library version you depend on.
##Signs and Symptoms
A reviewer spots a Hallucinated API when the code reads fluently and "looks like" idiomatic use of a library, yet the specific call, option, or import cannot be found in that library's real API. Tell-tale signs:
- A method or option whose name is too convenient — it does exactly what the prompt asked, with a name that blends two real APIs (e.g.
findLastWhere,includesAll,parseDateSafe). - Invented parameters on a real function (the dangerous variant — it often compiles and only fails at runtime, or silently ignores the option).
- An
importof a package that is not inpackage.json/requirements.txt, or a named import that the module never exports. - API shapes that mix versions: a v2 signature called on a v5 client, or a method removed/renamed several releases ago.
- Confident inline comments asserting the call is correct ("// returns the refunded charge").
// AI-generated — fluent, plausible, and wrong
import { formatRelative } from 'date-fns';
// `roundingMethod` is invented here — formatRelative accepts no such option (it belongs to formatDistanceStrict), so it is silently ignored
const label = formatRelative(date, new Date(), { roundingMethod: 'floor' });
// `findLastWhere` does not exist on Array — runtime TypeError
const lastActive = items.findLastWhere(i => i.active);
// Invented SDK method: stripe-node has no refunds.createForCharge — the real call is stripe.refunds.create({ charge, amount })
await stripe.refunds.createForCharge(chargeId, { amount: 500 });
A fast heuristic: if you cannot point to the doc entry or type definition for a third-party call in under a minute, treat it as hallucinated until proven otherwise.
##Reasons for the Problem
Why models produce it
- Next-token plausibility, not lookup. An LLM predicts the most statistically likely continuation, which is effectively the average of every similar API it has seen. That average is often a method that ought to exist — the model emits "the convenient name" rather than the real one. In one study of API recommendation for Java 8 (JRE), 58.1%–84.1% of the APIs recommended by three models did not exist in the specified library, and most errors were non-existent method names (arXiv 2409.15228).
- Version blending. Training corpora mix many versions of a library, so the model fuses v2 and v5 signatures into one that matches neither. API knowledge conflicts, such as calling a non-existent parameter, are a documented category of repository-level hallucination (Zhang et al., ISSTA 2025), and the authors note that some hallucinations "can likely pass static checks and all test cases."
- Training-cutoff staleness. The model confidently uses APIs renamed, deprecated, or removed since its cutoff, and reaches for whatever appeared most often in its training corpus — which may be an older version of the library than the one you have installed.
- No repository/dependency context. Without your
package.jsonor the actual module source, the model invents helpers that "feel" like they belong to your stack. - Sycophancy / eagerness to answer. The assistant almost never says "I'm not sure that method exists" — it produces confident, runnable-looking code, which lowers reviewer suspicion.
- Determinism makes it exploitable. Package hallucination is not random noise: across 16 models and 576,000 code samples (2.23M package references), 19.7% of package references were hallucinated (205,474 unique non-existent names), and 58% of hallucinated packages were repeated more than once across 10 re-runs of the same prompt (Spracklen et al., USENIX Security 2025; SecurityWeek summary).
Why it hurts
- Correctness. Invented parameters and silently-ignored options produce wrong behavior on code paths that tests rarely cover; the failure surfaces in production, not at compile time.
- Security / supply chain. A hallucinated package name is a registration target: attackers publish malware under the predicted name, so the next developer who accepts the suggestion installs it — the slopsquatting attack (term coined by PSF's Seth Larson). The staleness variant silently reintroduces deprecated or CVE-bearing APIs.
- Review load. Plausible code shifts the burden onto reviewers to verify every unfamiliar call against docs; fluent prose makes that verification less likely to happen.
- Tech-debt accrual. Developers often paste the "almost working" hallucinated stub and patch around it rather than fix the root call — feeding the broader AI-era trend of rising copy/paste and falling refactoring (GitClear 2025: moved/refactored lines 25% (2021) → under 10% (2024); copy/pasted 8.3% → 12.3%; 5+-line duplicate blocks ~8× in 2024).
##Treatment
Process & prompting tactics
- Ground the model in the real API. Paste the actual type stubs, the relevant doc page, or the installed version's source into context, or use a docs-retrieval tool (Context7-style / RAG). Tell it the exact dependency versions from your lockfile.
- Require citations. Ask the model to name the official doc entry or type signature for every third-party call it uses. Practitioner rule of thumb: every method call against a third-party library should be traced to its documentation entry before the PR is approved.
- Make it run, in the loop. Require
tsc/mypy/pylint/ the build and the test suite to pass, and have the agent actuallynpm install/pip installso a hallucinated package fails fast instead of reaching review. - Ask it to reuse, not invent. Feed it a
grepof the existing module and instruct it to call existing helpers (reuse the helpers in utils/http.ts) rather than conjure new ones — this also counters the related reinvented-helper duplication smell. - Gate dependencies. Use a lockfile + an install allowlist and a supply-chain scanner (Socket/Snyk) before any new package is added, so slopsquatted names cannot slip in.
The code refactor
Replace the invented call with the verified real one. If you genuinely want the convenience the model imagined, implement it once against the real API behind a wrapper created with Extract Method instead of scattering the fake call.
// Before — invented SDK method + invalid option value
async function refundLast(chargeId: string) {
// `stripe.refunds.createForCharge` does not exist, and 'requested' is not a valid refund reason
return stripe.refunds.createForCharge(chargeId, { amount: 500, reason: 'requested' });
}
// After — verified against the installed Stripe SDK's types/docs,
// and the "convenience" wrapped once so the real API isn't repeated
async function refundCharge(chargeId: string, amountCents: number) {
return stripe.refunds.create({
charge: chargeId,
amount: amountCents,
reason: 'requested_by_customer',
});
}
For the staleness variant, treat a flagged deprecated call as a real upgrade task: move to the current API and pin the version, rather than silencing the warning.
Limits. Type-checkers and resolvers catch the resolvable subset (typed objects, unresolved imports). The dynamic/untyped remainder — invented options on any, string config keys, REST payload fields — has no reliable automated detector and must be verified by a human against the real docs.
##Detected by
- eslint-plugin-import import/no-unresolved — Unresolved import
- eslint-plugin-import import/named — Non-existent named export
- Pylint no-member (E1101) — Access to undefined member
- mypy attr-defined — Attribute/method not defined on type