OpenAI max_tokens vs Context Window — Room for Reply

OpenAI LLM API

The model stops mid-sentence. Finish reason is length. Your prompt was a short question plus a pasted 40-page PDF dump. You set max_tokens: 2048 thinking that reserved two thousand tokens for the answer. Instead the API counted the prompt against the context window, left almost no room, and truncated. max_tokens (or max_completion_tokens) caps the completion; it does not expand the context window.

Two different budgets

ConceptMeaning
Context windowHard limit on prompt + completion (+ tool overhead) for the model
max_tokens / max_completion_tokensUpper bound on generated tokens for this response
Prompt tokensEverything you already sent: system, history, tools, files

Approximate invariant:

prompt_tokens + completion_tokens ≤ context_window
completion_tokens ≤ max_tokens

If prompt_tokens is 120_000 on a 128k model, the model can emit at most ~8k tokens even if you asked for max_tokens: 16000. Some APIs will error; others silently clamp. Either way you do not get a 16k essay.

Why replies die early

  1. Chat history bloat — You append every turn without summarization. By turn 30 the window is mostly old tool JSON.
  2. RAG over-retrieval — Top-20 chunks × long documents leave no space for the answer.
  3. System prompts with novels — Policy text + few-shot examples eat 4–8k before the user speaks.
  4. Wrong parameter — Setting a high max_tokens while the prompt already filled the window only states an impossible wish.

Measure before you guess

import { encoding_for_model } from "tiktoken"; // or a modern tokenizer for your model

function count(text: string, model: string) {
  const enc = encoding_for_model(model as any);
  try {
    return enc.encode(text).length;
  } finally {
    enc.free();
  }
}

Log usage.prompt_tokens and usage.completion_tokens from the API response. When finish_reason === "length", compare prompt size to model limit. Also use a token counter for rough local estimates before you burn paid calls.

Budgeting pattern that works

const MODEL_LIMIT = 128_000;
const RESERVE_FOR_ANSWER = 2_048;
const SAFETY = 512;

function fitPrompt(parts: string[]): string {
  const budget = MODEL_LIMIT - RESERVE_FOR_ANSWER - SAFETY;
  // keep system + latest user; truncate or drop oldest history / RAG hits
  // ...
  return assembled;
}

const completion = await client.chat.completions.create({
  model: "gpt-4.1",
  messages,
  max_completion_tokens: RESERVE_FOR_ANSWER,
});

Rules of thumb:

  • Reserve answer space first, then fill the prompt with what fits.
  • Prefer summarize old turns over sending full history.
  • Cap RAG characters aggressively; cite pointers instead of pasting whole files.
  • For long outputs, use continuation (finish_reason === "length" → ask to continue) only after you verify the window actually has room.

API naming drift

Older Chat Completions used max_tokens. Newer models/APIs prefer max_completion_tokens or Responses API equivalents. Reasoning models may spend hidden tokens on chain-of-thought that also compete with visible output — read the model’s docs for whether reasoning counts against the same cap. Treat vendor docs as source of truth; the invariant “prompt + out ≤ window” still guides debugging.

Failure mode cheat sheet

ObservationLikely cause
finish_reason: length, short answerPrompt too large or max_* too small
API 400 context length exceededPrompt alone past the window
Good first paragraph then cutHit max_tokens while window still had room — raise completion budget
Quality drops late in long chatsHistory noise; not a tokenizer bug

Room for the reply is a first-class budget. Allocate it explicitly. Everything else — RAG, memory, tools — competes for what remains.