One token at a time

A code LLM is a large language model used to write, complete or explain source code. Most are general models whose training data included huge amounts of public code next to ordinary text; some are further tuned for coding. The assistant you chat with and the one built into a code editor are usually the same kind of model.

To the model, code is text. It writes a function exactly as it writes an email: by next-token prediction, scoring every possible next token (a short piece of text, often part of a word or a symbol such as (), choosing one, appending it and repeating. How the model learned to score tokens is explained in Transformers and Large Language Models. Three things follow for code:

  • It is not run. While writing, the model does not execute anything, open a browser or check that a function exists. It writes what usually comes next in code like yours. Only a coding agent with tools can run code and read the errors, and then only after writing it (see the agent loop and Vibe Coding Tools).
  • Early choices shape the rest. Once the model has written const clock = …, the following lines are predicted to fit that choice. A wrong decision in line 3 is carried through line 300.
  • Common patterns win. What appears often in public code is what the model tends to write. That is usually good (common code is often well-tested code), but it also means old habits that are still common online keep coming back.

Plausible is not the same as correct

Plausible-looking code is code that has every outward sign of a correct solution, such as sensible names, tidy structure, helpful comments and the right libraries, whether or not it actually does the job. A language model produces plausible-looking code very reliably, because looking like typical code is exactly what it is trained to do. Whether the code is correct is a separate question that the text itself cannot answer.

An example from games. Many older tutorials move an object by a fixed amount every frame:

// Older pattern: do not copy. The ship moves 0.1 per frame, so it
// moves 2.4 times as fast on a 144 Hz screen as on a 60 Hz screen.
ship.position.x += 0.1;

It looks fine, it runs, and on the screen of the person who wrote it, it plays fine. On a 144 Hz monitor the game runs much faster, because there are more frames per second. The fix, moving by speed × the time since the last frame, is part of every step of this topic's game (see Build 2 · A Ship You Can Fly). Nothing in the first version hints that it is wrong; you only find out by trying it on another screen, or by knowing the pattern.

Same prompt, different code

At each step the model produces probabilities for the next token, and the application usually draws one at random according to those probabilities instead of always taking the most likely one (see token sampling). Output variation is the result: the same prompt, sent again, gives a different answer, and for code that can mean different variable names, a different structure, or a different library function. Microsoft's guidance for Copilot puts the practical rule in two words: expect variation.

The example below shows the mechanism with three invented candidates for the next token after const timer = new THREE., and five fixed numbers standing in for the random draws of five separate chats:

// Made-up probabilities for the next token after the text
//   const timer = new THREE.
// A real model scores every token in its vocabulary; these three are
// invented to show the mechanism.
const candidates = [
  ["Clock", 0.55], // common in older code, deprecated since r183
  ["Timer", 0.4], // what current three.js recommends
  ["GameClock", 0.05], // does not exist in three.js at all
];

// Pick a token: walk through the candidates until the running total
// passes the random draw (a number from 0 to 1).
function pickToken(candidates, draw) {
  let total = 0;
  for (const [token, probability] of candidates) {
    total += probability;
    if (draw < total) return token;
  }
  return candidates[candidates.length - 1][0];
}

// Five chats with the same prompt. Fixed draws stand in for the random
// numbers a real system would use, so the output is the same every run.
const draws = [0.31, 0.72, 0.18, 0.97, 0.47];
draws.forEach((draw, i) => {
  const token = pickToken(candidates, draw);
  console.log(`Chat ${i + 1}: const timer = new THREE.${token}();`);
});
Chat 1: const timer = new THREE.Clock();
Chat 2: const timer = new THREE.Timer();
Chat 3: const timer = new THREE.Clock();
Chat 4: const timer = new THREE.GameClock();
Chat 5: const timer = new THREE.Clock();

The numbers are made up, but the pattern is real: the same request can come back with a current function, a deprecated one or one that does not exist, and each answer is written with the same calm confidence. Over hundreds of tokens, small differences add up to visibly different programs. For vibe coding this has two practical consequences:

  • Asking again is not a neutral act. If you ask for the whole file again to fix one thing, other parts you were happy with may come back changed. Ask for one change at a time (see Prompting in Small Steps).
  • Keep the version that works. You cannot get the same answer back by sending the same prompt. Once something works, save it before you ask for more (see Saving Versions and Going Back).

Confident errors

A confident error is a mistake in an AI answer that is stated with the same certainty, and often the same detailed explanation, as a correct answer. Language models have no built-in signal that says "I am unsure here" in the text; the wording sounds equally sure whether the content is right or wrong. The general phenomenon is called hallucination; in code it has some typical forms.

A hallucinated API is a function, method, option or class that a model uses in generated code but that does not exist in the library, such as THREE.GameClock in the example above or an option that sounds right but was never there. In JavaScript the result is usually an error at run time ("… is not a function", "… is not a constructor"); sometimes the made-up option is simply ignored and nothing visibly happens, which is harder to notice.

The same can happen with whole packages. A study published at USENIX Security 2025 (Spracklen et al.) tested 16 models from 2023 and 2024 on Python and JavaScript code and found that on average 19.7% of the packages they recommended did not exist. The rate for current models is not known, and the numbers are discussed on Risks of Vibe Coding and in package hallucination. In this topic's game the only import is three; if an answer imports anything else, stop and ask why before you paste it.

Confident errors are dangerous because people believe them. In a Stanford study presented at ACM CCS 2023 (Perry et al., 47 participants), people who used an AI assistant wrote significantly less secure code than those who did not, and were also more likely to believe they had written secure code. Feeling sure and being right had come apart. That is the reason this topic keeps asking you to look, run and check, and not to rely on how convincing an answer sounds (see overreliance and Reducing Hallucinations).

Outdated APIs and the knowledge cut-off

A model's knowledge cut-off is the date up to which its training data was collected. It knows nothing about later releases, renamed functions or new warnings, unless the application gives it a search tool or you paste the documentation into the chat. The general limits are on Limits and Myths; for code they show up as stale APIs.

A stale API is a function, option or way of using a library that was correct in older versions but has since been deprecated, changed or removed, and that a model still suggests because it was common in its training data. Libraries change faster than models are retrained, and old code stays online for years, so this is one of the most frequent problems with generated code.

Three.js, the 3D library this topic uses, has a clear example (as of October 2026, current release r186, npm version three@0.186.1). For years, tutorials measured the time between frames with THREE.Clock. Since r183 (February 2026) Clock is deprecated, and r186 prints a warning ending in "Please use THREE.Timer instead." A model trained mostly on older code may well write this:

// Older code: do not copy. THREE.Clock is deprecated since r183,
// and this loop calls requestAnimationFrame itself.
const clock = new THREE.Clock();

function animate() {
  requestAnimationFrame(animate);
  const dt = clock.getDelta();
  renderer.render(scene, camera);
}
animate();

It still works, with a warning in the console, which is exactly why it survives: nothing breaks. The current way, from step 1 of this topic's game (script.js), uses THREE.Timer and lets the renderer run the loop with setAnimationLoop, which the three.js source advises instead of calling requestAnimationFrame() yourself:

renderer.setAnimationLoop((time) => {
  timer.update(time);
  const dt = clampStep(timer.getDelta()); // seconds since the last frame

  // Speeds are "per second" times dt, so the game runs at the same pace
  // on a 60 Hz and a 144 Hz screen.
  updateStars(dt);

  renderer.render(scene, camera);
});

Other examples are not always so gentle. The old lighting switch renderer.useLegacyLights was deprecated in r155 and no longer exists in r186. Code that sets it raises no error and does nothing: the lighting follows the newer, physically based model, whatever the tutorial intended. Silent mistakes like this are harder to spot than a warning.

The best defense is cheap: name the current way in the prompt.

- Measure time with THREE.Timer (not THREE.Clock, which is deprecated):
  timer.connect(document), then timer.update(time) and timer.getDelta()
  every frame.

Beyond that: read the console after every paste (a deprecation warning is a free hint), and when a function looks unfamiliar, check it in the library's own documentation, not by asking the same model whether it exists.

What the model knows about your project

In this topic you build the game in CodePen and talk to the AI in a separate chat. At its 2.0 launch in July 2026, CodePen said it "doesn't have any built-in AI features yet", so the two do not see each other. The model knows exactly what is in the conversation and nothing else:

  • Only what you paste. If you paste script.js but not index.html, the model does not know which ids your elements have, and it will guess.
  • Not what happened. It cannot see the preview, the console or the error. If you do not paste the error message, it does not exist for the model (see Debugging with AI).
  • Not your other chats. A new chat starts from nothing. Within one chat, the application sends the whole conversation again with every message (see stateless models).
  • Not everything in a long chat equally well. As a conversation grows, models pay less reliable attention to its details, especially in the middle (see context rot and lost in the middle). After many rounds, the third version of your file and the seventh may get mixed up.

These limits are the reasons behind the working habits in the rest of this topic. A small change is easier for you to check, needs less context for the model to get right, and is cheap to undo when it goes wrong. Pasting the current file instead of saying "the code from before" removes guesswork. And starting a new chat with a short summary of the project, when the old one has grown long, is often better than going on (see Writing Your Own Prompt and Breaking Down Tasks).

Try it yourself

Open a new chat in an AI assistant (Claude, ChatGPT, Copilot, Gemini) and paste the first prompt. Then open a second new chat and paste the same prompt again, so the first answer cannot influence the second. Use a third new chat for the second prompt.

Write a JavaScript function that formats a game score as a string with exactly six digits, padded with leading zeros (for example 4250 becomes "004250"). Scores above 999999 should show as "999999". Give only the function and three example calls.
Write a minimal Three.js example in one JavaScript module: a rotating cube that turns at the same speed on a 60 Hz and a 144 Hz screen. Use import * as THREE from "three". Explain in two sentences how you measure the time between frames.

What to look for: in the two answers to prompt 1, how many things differ (the function name, the method used to pad, how the limit is handled, the comments), and whether both really handle 999999, 1000000 and 0 correctly; run them in the browser console to find out. In prompt 2, whether the answer uses THREE.Clock or THREE.Timer, requestAnimationFrame or renderer.setAnimationLoop, and whether it mentions that Clock is deprecated. Either way, the answer will sound sure of itself.