Diagnostic LLM APIs
The 1M context window is capped at 200k in your own code
The long-context work took a quarter. There is a constant called MAX_CONTEXT_TOKENS, there is a guard that truncates the retrieved documents before they reach it, there is a branch that routes anything over the line to a summarise-first path, and there is a comment above all of it citing a beta header. Every one of those was correct when it was written. The model ids in the config have changed four times since, the window they now report is a million tokens, and the constant still says two hundred thousand, so the truncation still fires, the summarise path still runs, and the capability the company is paying for stops at the same place it stopped eighteen months ago.
Ask the API what the window actually is, per id. GET /v1/models/{model_id} with a workspace key returns max_input_tokens, and it is authoritative: 200000 or 1000000, no interpretation required. Then compare it with the ceiling your application enforces, which you have to declare in a small config file, because nothing in either API can read your source tree.
Three things go wrong at that comparison and they have three different repairs. A model reporting 1000000 against a code ceiling of 200000 is a window bought and capped in software. A model reporting 1000000 where the code still sends anthropic-beta: context-1m-2025-08-07 has an inert header: 1M is the default on Opus 5, Opus 4.8, 4.7 and 4.6, Sonnet 5 and 4.6, Fable 5, Mythos 5 and Mythos Preview, and the beta does nothing. A model reporting 200000 where the code sends that same header is worse: the beta was retired for Claude Sonnet 4.5 and Claude Sonnet 4 on 30 April 2026, so anything over 200k now returns 400 invalid_request_error with prompt is too long, header or no header.
The fourth thing is a price branch. There is no long-context premium on current models: a 900k-token request bills at the same per-token rate as a 9k one, and the dedicated 1M rate limits were removed too. Code that still charges or throttles for a premium is enforcing a rule that no longer exists.
Probing the header proves nothing, so this script does not. GET /v1/models with anthropic-beta: context-1m-2025-08-07 still returns 200, because the beta name is still recognised. Acceptance is not effect.
The problem in plain words
This is the failure mode of a capability that graduated. When the 1M window was a beta it needed a header, it had its own rate limits, and on some models it carried a premium, so every serious integration grew code to handle all three. Then it became the default, the beta was retired on the models where it had been a beta, the separate limits were removed and the premium went away — and none of those changes produce an error in an application that is still being careful about them. Careful code keeps working. It just keeps working at 200k.
What makes it durable is that the constant is defensible. Two hundred thousand is a real number, it was correct, and it is protecting you from a real error on the models where it still applies. Nobody deletes a guard that has never failed. Meanwhile the model ids in the config rotate every few months as part of ordinary maintenance, and each rotation moves the true ceiling without touching the enforced one, because they live in different files and nothing joins them.
The visible symptoms are all downstream and all easy to misattribute. Documents get truncated, so retrieval quality is blamed. A summarise-first path runs on inputs that never needed it, so latency and cost are blamed on the summariser. And on Sonnet 4.5 or Sonnet 4 the error is a real 400 that says prompt is too long while the request carries the header that was supposed to prevent exactly that, which reads as a provider bug and is not one.
Why it happens
The model object is the only source that cannot go stale. Anthropic's GET /v1/models/{id} returns max_input_tokens and max_output_tokens per id. A docs table lags, a blog post is a snapshot, and a constant in your repo is a memory of a fact rather than the fact. Read the number from the id you actually send and the whole class of drift disappears.
The header probe is a non-signal, and knowing that saves you a wrong conclusion. Sending anthropic-beta: context-1m-2025-08-07 on a read-only GET /v1/models returns 200 whether or not the beta does anything on any model, because the name is still valid. A script that treated that 200 as confirmation would report health on precisely the configuration that is broken. This one refuses to make the call and says why.
A ceiling that is too low and a ceiling that is too high are opposite findings. Enforcing 200k against a 1M model wastes the window silently. Enforcing 1M against a 200k model produces a hard 400 on the first long request, which is loud, and which the overflow note covers properly by counting an actual payload. This script grades both, because they come out of the same comparison, and hands the second one on rather than restating it.
The output ceiling is a different number and a different note. Every model with a 1M window is still capped at 128k output tokens per request, and a configured max_tokens above a model's own max_output_tokens is its own failure with its own repair. That belongs to the output-cap note, so this script prints the reported output ceiling as context and does not grade it.
This is not the same question as how much long traffic you send. Grouping the usage report by context_window tells you how much of your input actually crosses 200k, which is a size alarm about prefixes that grow; that note owns it. This one is about a ceiling: a window that is available, paid for, and unreachable because a constant in your own code says so. You can have the second problem while sending no long requests at all — in fact that is exactly what it looks like.
The fix, as a flow
This is what a capability looks like after it graduates. The beta header, the separate rate limits and the price premium all went away, and none of those removals produces an error in an application that is still being careful about them. Careful code keeps working. It keeps working at two hundred thousand tokens.
How to fix it
Write down the ceiling your code actually enforces
A small JSON file keyed by model id: the input ceiling the application applies, any anthropic-beta values it still sends, and whether a long-context price or throttle branch still exists. Nothing in either API can read your source, so this half of the comparison has to be declared. Declaring it is also the first time most teams look at all four facts together.
Read max_input_tokens per id from the Models API
GET /v1/models/{model_id} with a workspace key, one call per id in the config. The ids are validated before they reach the URL, because a value out of a config file becoming a path segment is one typo away from requesting something else entirely. A 404 means the id no longer resolves, which is a different note.
Compare the two ceilings in both directions
Reported 1M against enforced 200k is the headline: the window is bought and capped. Enforced above reported is the opposite fault and will 400 on the first long request. Equal is aligned, and gets one quiet line.
Grade every beta header against the model it is sent on
On a model that reports 1000000, context-1m-2025-08-07 is inert and should be deleted. On a model that reports 200000, the same header is a retired beta and the code around it is relying on something that stopped working on 30 April 2026.
Print the repair per model, including the branches to delete
The constant to raise, the header to remove, the premium branch that prices something that no longer costs extra, and the separate long-context rate limit that no longer exists. All printed. Raising a context ceiling is a deploy with a blast radius and an owner.
How to check it worked
Re-run after the constant moves. The reported window will not change — it was never the thing that was wrong — but every model should come back aligned, and the truncation path should stop firing on inputs it was never supposed to catch.
python3 anthropic_context_window_cap.py --config context_rules.json
# capped-in-code claude-opus-5 model reports 1000000, code enforces 200000: 800000 token(s) bought and unreachable
# repair: raise the enforced ceiling to the reported window, then delete the truncation path that exists to serve the old one.
# inert-beta-header claude-opus-5 context-1m-2025-08-07 is sent here and does nothing: 1M is the default on this model
# retired-beta claude-sonnet-4-5 model reports 200000 and the beta was retired for this family on 2026-04-30
# repair: over 200k this id now returns 400 prompt is too long, header or not. The path forward is Sonnet 4.6 or later.
# phantom-premium claude-opus-5 a long-context price branch is declared, and there is no long-context premium
# aligned claude-haiku-4-5-20251001 model reports 200000, code enforces 200000
# 4 model(s) checked, 3 finding(s). Output ceilings are reported and not graded.
The full code
One GET per configured model id and no probe, because the probe would lie. Seven pure functions: the rules parser, which is also the guard that stops a config value becoming a URL path segment; the window and output readers, which return None rather than a default when the field is absent; the shortfall arithmetic; the beta grader, which needs the reported window to know whether a header is inert or retired; the premium grader; and the audit, which returns a list of findings per model rather than one state, because a single misconfigured id routinely has three of them at once.
"""Compare the context window a Claude model reports with the one your code enforces.
Read only. One GET per configured model id against the Models API with a
workspace key. No message is ever sent and no long request is constructed.
The API cannot read your source tree, so the enforced ceiling, the beta headers
still in the request path and any surviving long-context price branch are
declared in a small JSON file. That declaration is half the comparison and the
script says so rather than pretending to have discovered it.
There is deliberately no beta-header probe. GET /v1/models with
anthropic-beta: context-1m-2025-08-07 returns 200 whether or not the beta does
anything on any model, because the name is still recognised. Acceptance is not
effect, and a script that read that 200 as confirmation would report health on
exactly the configuration that is broken.
Every repair is printed. Raising a context ceiling is a deploy.
"""
import argparse
import json
import logging
import os
import sys
import requests
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
log = logging.getLogger("anthropic_context_window_cap")
API = "https://api.anthropic.com/v1"
VERSION = "2023-06-01"
BETA_1M = "context-1m-2025-08-07"
BETA_RETIRED_ON = "2026-04-30"
STANDARD_WINDOW = 200_000
LONG_WINDOW = 1_000_000
FINDINGS = ("capped-in-code", "ceiling-below-model", "cap-above-model",
"inert-beta-header", "retired-beta", "phantom-premium")
def _int(value):
"""Read a positive integer, or None. Pure. Absent is never zero.
None and 0 mean very different things about a context window, and a reader
that collapses them reports a model with no window rather than a model that
did not say.
"""
try:
number = int(value)
except (TypeError, ValueError):
return None
return number if number > 0 else None
def valid_model_id(model_id):
"""Is this a plausible model id? Pure.
The guard that stops a line of a config file becoming a URL path segment.
Model ids are letters, digits, hyphens, dots and underscores, and anything
else is discarded rather than sent.
"""
text = str(model_id or "").strip()
if not text or len(text) > 128:
return False
if not text[0].isalpha():
return False
return all(ch.isalnum() or ch in "-._" for ch in text)
def parse_rules(config):
"""Read the declared per-model rules. Pure. Invalid ids are dropped.
Each rule carries the enforced input ceiling, any anthropic-beta values the
request path still sends, and whether a long-context price or throttle
branch still exists.
"""
rules = {}
for model_id, raw in (config or {}).items():
if not valid_model_id(model_id):
continue
row = raw if isinstance(raw, dict) else {}
betas = row.get("beta_headers")
if isinstance(betas, str):
betas = [betas]
rules[str(model_id).strip()] = {
"cap": _int(row.get("max_input_tokens")),
"betas": [str(b).strip().lower() for b in (betas or [])
if str(b or "").strip()],
"premium": bool(row.get("long_context_premium")),
}
return rules
def reported_window(model):
"""max_input_tokens off the model object. Pure. None when absent."""
return _int((model or {}).get("max_input_tokens"))
def reported_output(model):
"""max_output_tokens off the model object. Pure. Context, never graded here."""
return _int((model or {}).get("max_output_tokens"))
def shortfall(reported, enforced):
"""Tokens of window that exist and cannot be reached. Pure. None when unknown."""
if reported is None or enforced is None:
return None
return max(0, reported - enforced)
def grade_ceiling(reported, enforced):
"""The enforced ceiling against the reported window. Pure. (state, detail) or None."""
if reported is None:
return ("window-not-reported",
"the model object carried no max_input_tokens, so no claim is "
"made about the enforced ceiling")
if enforced is None:
return None
if enforced > reported:
return ("cap-above-model",
"model reports %d, code enforces %d: the first request over "
"the reported window returns 400 prompt is too long"
% (reported, enforced))
gap = shortfall(reported, enforced)
if gap == 0:
return ("aligned", "model reports %d, code enforces %d"
% (reported, enforced))
if reported >= LONG_WINDOW and enforced <= STANDARD_WINDOW:
return ("capped-in-code",
"model reports %d, code enforces %d: %d token(s) of window "
"bought and unreachable" % (reported, enforced, gap))
return ("ceiling-below-model",
"model reports %d, code enforces %d: %d token(s) of window left "
"unused" % (reported, enforced, gap))
def grade_betas(reported, betas):
"""Every declared beta header against the window the model reports. Pure.
The same header name is two different findings depending on the model. On a
model that already defaults to 1M it is inert. On one that reports the
standard window it is a retired beta, and the code around it is relying on
something that stopped working.
"""
out = []
for beta in betas or []:
if beta != BETA_1M:
continue
if reported is None:
continue
if reported >= LONG_WINDOW:
out.append(("inert-beta-header",
"%s is sent here and does nothing: the 1M window is "
"the default on this model and needs no header"
% BETA_1M))
else:
out.append(("retired-beta",
"model reports %d and %s was retired for the Sonnet "
"4.5 and Sonnet 4 family on %s: over the standard "
"window this id now returns 400, header or not"
% (reported, BETA_1M, BETA_RETIRED_ON)))
return out
def grade_premium(reported, premium):
"""A surviving long-context price or throttle branch. Pure. None when absent."""
if not premium:
return None
if reported is None or reported < LONG_WINDOW:
return None
return ("phantom-premium",
"a long-context price or throttle branch is declared for this "
"model, and there is no long-context premium: a 900k-token request "
"bills at the same per-token rate as a 9k one, and the dedicated "
"1M rate limits were removed")
def audit(model, rule):
"""Every finding for one model. Pure. Returns a list of (state, detail).
A list rather than a single state on purpose. One stale id routinely
carries three at once - a ceiling frozen at 200k, an inert header, and a
premium branch pricing something that is free - and collapsing them to the
first would hide two repairs behind one line.
"""
rule = rule or {}
reported = reported_window(model)
out = []
ceiling = grade_ceiling(reported, rule.get("cap"))
if ceiling is not None:
out.append(ceiling)
out.extend(grade_betas(reported, rule.get("betas")))
premium = grade_premium(reported, rule.get("premium"))
if premium is not None:
out.append(premium)
return out
def repair_lines(state, model_id):
"""The repair for one finding. Pure. Printed, never performed."""
if state == "capped-in-code":
return [
"raise the enforced ceiling for %s to the window the model "
"reports, then delete the truncation path that exists to serve "
"the old one." % model_id,
"read the ceiling from the model object at start-up instead of "
"hardcoding it, and this cannot drift again when the id rotates.",
]
if state == "ceiling-below-model":
return ["the enforced ceiling for %s is below the reported window. "
"Confirm that is deliberate rather than inherited." % model_id]
if state == "cap-above-model":
return ["this direction fails loudly rather than quietly: count a real "
"payload against the reported window before you send it."]
if state == "inert-beta-header":
return ["delete %s from the request path for %s. It is not harmful and "
"it is not doing anything, and leaving it in is what keeps the "
"rest of the obsolete branch alive." % (BETA_1M, model_id)]
if state == "retired-beta":
return ["over the standard window this id now returns 400 whatever the "
"header says. The path forward is a 4.6 or later id, where 1M "
"is the default and no header is involved."]
if state == "phantom-premium":
return ["delete the premium branch and the separate long-context "
"throttle. Standard account rate limits apply at every context "
"length now."]
return []
def get(session, path):
r = session.get(API + path, timeout=60)
if r.status_code in (401, 403):
raise SystemExit("%d from Anthropic: ANTHROPIC_API_KEY needs to be a "
"workspace key that can read the Models API"
% r.status_code)
if r.status_code == 404:
return None
r.raise_for_status()
return r.json()
def main():
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("--config", required=True, metavar="FILE",
help="JSON: model id to the ceiling your code enforces, "
"the anthropic-beta values it sends, and whether a "
"long-context price branch still exists")
args = ap.parse_args()
key = os.environ.get("ANTHROPIC_API_KEY")
if not key:
log.error("set ANTHROPIC_API_KEY to a workspace key that can read the "
"Models API")
return 2
try:
with open(args.config, "r", encoding="utf-8") as fh:
rules = parse_rules(json.load(fh))
except (OSError, ValueError) as exc:
log.error("could not read %s: %s", args.config, exc)
return 2
if not rules:
log.error("no valid model ids in %s", args.config)
return 2
session = requests.Session()
session.headers.update({"x-api-key": key, "anthropic-version": VERSION})
log.info("no beta-header probe is made: %s is still a recognised name, so "
"a 200 would prove the name is valid and nothing about its effect",
BETA_1M)
bad = 0
for model_id in sorted(rules):
model = get(session, "/models/" + model_id)
if model is None:
log.warning("%-20s %-26s the id no longer resolves on the Models "
"API, which is a retirement rather than a ceiling "
"problem", "unknown-model-id", model_id)
bad += 1
continue
for state, detail in audit(model, rules[model_id]):
line = "%-20s %-26s %s" % (state, model_id, detail)
if state in FINDINGS:
bad += 1
log.warning(line)
for repair in repair_lines(state, model_id):
log.warning(" repair: %s", repair)
else:
log.info(line)
out = reported_output(model)
if out is not None:
log.info("%-20s %-26s reports max_output_tokens %d, which is a "
"separate ceiling and is not graded here",
"output-ceiling", model_id, out)
log.info("%d model(s) checked, %d finding(s)", len(rules), bad)
return 1 if bad else 0
if __name__ == "__main__":
sys.exit(main())
/**
* Compare the context window a Claude model reports with the one your code enforces.
*
* Read only. One GET per configured model id against the Models API with a
* workspace key. No message is ever sent.
*
* There is deliberately no beta-header probe. GET /v1/models with
* anthropic-beta: context-1m-2025-08-07 returns 200 whether or not the beta
* does anything, because the name is still recognised. Acceptance is not
* effect.
*/
import { readFile } from 'node:fs/promises';
const API = 'https://api.anthropic.com/v1';
const VERSION = '2023-06-01';
const BETA_1M = 'context-1m-2025-08-07';
const BETA_RETIRED_ON = '2026-04-30';
const STANDARD_WINDOW = 200_000;
const LONG_WINDOW = 1_000_000;
const FINDINGS = new Set(['capped-in-code', 'ceiling-below-model',
'cap-above-model', 'inert-beta-header',
'retired-beta', 'phantom-premium']);
/** Read a positive integer, or null. Pure. Absent is never zero. */
export function readInt(value) {
const n = Number(value);
if (!Number.isFinite(n)) return null;
const whole = Math.trunc(n);
return whole > 0 ? whole : null;
}
/**
* Is this a plausible model id? Pure.
* The guard that stops a config value becoming a URL path segment.
*/
export function validModelId(modelId) {
const text = String(modelId ?? '').trim();
if (!text || text.length > 128) return false;
return /^[A-Za-z][A-Za-z0-9._-]*$/.test(text);
}
/** Read the declared per-model rules. Pure. Invalid ids are dropped. */
export function parseRules(config) {
const rules = {};
for (const [modelId, raw] of Object.entries(config ?? {})) {
if (!validModelId(modelId)) continue;
const row = raw && typeof raw === 'object' ? raw : {};
let betas = row.beta_headers;
if (typeof betas === 'string') betas = [betas];
rules[String(modelId).trim()] = {
cap: readInt(row.max_input_tokens),
betas: (betas ?? []).map((b) => String(b ?? '').trim().toLowerCase())
.filter((b) => b.length > 0),
premium: Boolean(row.long_context_premium),
};
}
return rules;
}
/** max_input_tokens off the model object. Pure. Null when absent. */
export function reportedWindow(model) {
return readInt(model?.max_input_tokens);
}
/** max_output_tokens off the model object. Pure. Context, never graded here. */
export function reportedOutput(model) {
return readInt(model?.max_output_tokens);
}
/** Tokens of window that exist and cannot be reached. Pure. Null when unknown. */
export function shortfall(reported, enforced) {
if (reported === null || enforced === null) return null;
return Math.max(0, reported - enforced);
}
/** The enforced ceiling against the reported window. Pure. [state, detail] or null. */
export function gradeCeiling(reported, enforced) {
if (reported === null) {
return ['window-not-reported',
'the model object carried no max_input_tokens, so no claim is made ' +
'about the enforced ceiling'];
}
if (enforced === null) return null;
if (enforced > reported) {
return ['cap-above-model',
`model reports ${reported}, code enforces ${enforced}: the first ` +
'request over the reported window returns 400 prompt is too long'];
}
const gap = shortfall(reported, enforced);
if (gap === 0) {
return ['aligned', `model reports ${reported}, code enforces ${enforced}`];
}
if (reported >= LONG_WINDOW && enforced <= STANDARD_WINDOW) {
return ['capped-in-code',
`model reports ${reported}, code enforces ${enforced}: ${gap} token(s) ` +
'of window bought and unreachable'];
}
return ['ceiling-below-model',
`model reports ${reported}, code enforces ${enforced}: ${gap} token(s) ` +
'of window left unused'];
}
/** Every declared beta header against the window the model reports. Pure. */
export function gradeBetas(reported, betas) {
const out = [];
for (const beta of betas ?? []) {
if (beta !== BETA_1M || reported === null) continue;
if (reported >= LONG_WINDOW) {
out.push(['inert-beta-header',
`${BETA_1M} is sent here and does nothing: the 1M window is the ` +
'default on this model and needs no header']);
} else {
out.push(['retired-beta',
`model reports ${reported} and ${BETA_1M} was retired for the Sonnet ` +
`4.5 and Sonnet 4 family on ${BETA_RETIRED_ON}: over the standard ` +
'window this id now returns 400, header or not']);
}
}
return out;
}
/** A surviving long-context price or throttle branch. Pure. Null when absent. */
export function gradePremium(reported, premium) {
if (!premium) return null;
if (reported === null || reported < LONG_WINDOW) return null;
return ['phantom-premium',
'a long-context price or throttle branch is declared for this model, and ' +
'there is no long-context premium: a 900k-token request bills at the same ' +
'per-token rate as a 9k one, and the dedicated 1M rate limits were removed'];
}
/**
* Every finding for one model. Pure. Returns a list of [state, detail].
* A list rather than one state: a stale id routinely carries a frozen ceiling,
* an inert header and a phantom premium at the same time.
*/
export function audit(model, rule) {
const row = rule ?? {};
const reported = reportedWindow(model);
const out = [];
const ceiling = gradeCeiling(reported, row.cap ?? null);
if (ceiling !== null) out.push(ceiling);
out.push(...gradeBetas(reported, row.betas));
const premium = gradePremium(reported, row.premium);
if (premium !== null) out.push(premium);
return out;
}
/** The repair for one finding. Pure. Printed, never performed. */
export function repairLines(state, modelId) {
if (state === 'capped-in-code') {
return [
`raise the enforced ceiling for ${modelId} to the window the model ` +
'reports, then delete the truncation path that exists to serve the old one.',
'read the ceiling from the model object at start-up instead of ' +
'hardcoding it, and this cannot drift again when the id rotates.',
];
}
if (state === 'ceiling-below-model') {
return [`the enforced ceiling for ${modelId} is below the reported window. ` +
'Confirm that is deliberate rather than inherited.'];
}
if (state === 'cap-above-model') {
return ['this direction fails loudly rather than quietly: count a real ' +
'payload against the reported window before you send it.'];
}
if (state === 'inert-beta-header') {
return [`delete ${BETA_1M} from the request path for ${modelId}. It is not ` +
'harmful and it is not doing anything, and leaving it in is what ' +
'keeps the rest of the obsolete branch alive.'];
}
if (state === 'retired-beta') {
return ['over the standard window this id now returns 400 whatever the ' +
'header says. The path forward is a 4.6 or later id, where 1M is ' +
'the default and no header is involved.'];
}
if (state === 'phantom-premium') {
return ['delete the premium branch and the separate long-context throttle. ' +
'Standard account rate limits apply at every context length now.'];
}
return [];
}
async function get(key, path) {
const res = await fetch(API + path, {
headers: { 'x-api-key': key, 'anthropic-version': VERSION },
});
if (res.status === 401 || res.status === 403) {
throw new Error(`${res.status} from Anthropic: ANTHROPIC_API_KEY needs to ` +
'be a workspace key that can read the Models API');
}
if (res.status === 404) return null;
if (!res.ok) throw new Error(`${res.status} from ${path}`);
return res.json();
}
async function main() {
const key = process.env.ANTHROPIC_API_KEY;
if (!key) {
console.error('set ANTHROPIC_API_KEY to a workspace key that can read the ' +
'Models API');
process.exitCode = 2;
return;
}
const file = process.argv.slice(2).find((a) => !a.startsWith('--'));
if (!file) {
console.error('pass a JSON file of per-model rules: enforced ceiling, beta ' +
'headers, and whether a long-context price branch exists');
process.exitCode = 2;
return;
}
let rules;
try {
rules = parseRules(JSON.parse(await readFile(file, 'utf8')));
} catch (err) {
console.error(`could not read ${file}: ${err.message}`);
process.exitCode = 2;
return;
}
const ids = Object.keys(rules).sort();
if (ids.length === 0) {
console.error(`no valid model ids in ${file}`);
process.exitCode = 2;
return;
}
console.log(`no beta-header probe is made: ${BETA_1M} is still a recognised ` +
'name, so a 200 would prove the name is valid and nothing about ' +
'its effect');
let bad = 0;
for (const modelId of ids) {
const model = await get(key, `/models/${modelId}`);
if (model === null) {
console.warn(`${'unknown-model-id'.padEnd(20)} ${modelId.padEnd(26)} the ` +
'id no longer resolves on the Models API, which is a ' +
'retirement rather than a ceiling problem');
bad += 1;
continue;
}
for (const [state, detail] of audit(model, rules[modelId])) {
const line = `${state.padEnd(20)} ${modelId.padEnd(26)} ${detail}`;
if (FINDINGS.has(state)) {
bad += 1;
console.warn(line);
for (const repair of repairLines(state, modelId)) {
console.warn(` repair: ${repair}`);
}
} else {
console.log(line);
}
}
const out = reportedOutput(model);
if (out !== null) {
console.log(`${'output-ceiling'.padEnd(20)} ${modelId.padEnd(26)} reports ` +
`max_output_tokens ${out}, which is a separate ceiling and ` +
'is not graded here');
}
}
console.log(`${ids.length} model(s) checked, ${bad} finding(s)`);
process.exitCode = bad ? 1 : 0;
}
if (import.meta.url === `file://${process.argv[1]}`) {
main().catch((err) => { console.error(err.message); process.exitCode = 2; });
}
Add a test
The headline test is one model object reporting 1000000 against a config that enforces 200000, and the assertion is on the shortfall: eight hundred thousand tokens that exist, are paid for and cannot be reached. Beside it is the test that keeps the two directions apart, because an enforced ceiling above the reported window is the opposite fault and fails loudly. Then the same beta header graded twice — inert on a model that already defaults to 1M, retired on one that reports the standard window — the premium branch that prices something free, the multiple-findings shape that stops one stale id hiding two repairs behind one line, and the id validation that stops a config value walking into a URL path.
from anthropic_context_window_cap import (audit, grade_betas, grade_ceiling,
grade_premium, parse_rules,
repair_lines, reported_output,
reported_window, shortfall,
valid_model_id)
OPUS_5 = {"id": "claude-opus-5", "max_input_tokens": 1_000_000,
"max_output_tokens": 128_000}
SONNET_4_5 = {"id": "claude-sonnet-4-5", "max_input_tokens": 200_000,
"max_output_tokens": 64_000}
HAIKU = {"id": "claude-haiku-4-5-20251001", "max_input_tokens": 200_000,
"max_output_tokens": 64_000}
def test_a_million_token_window_enforced_at_two_hundred_thousand():
# The note in one assertion. Nothing is misconfigured on the provider side
# and nothing errors; the window is simply unreachable because a constant
# in the application says so.
rules = parse_rules({"claude-opus-5": {"max_input_tokens": 200_000}})
state, detail = grade_ceiling(reported_window(OPUS_5),
rules["claude-opus-5"]["cap"])
assert state == "capped-in-code"
assert "800000 token(s) of window bought and unreachable" in detail
assert shortfall(1_000_000, 200_000) == 800_000
assert any("raise the enforced ceiling" in line
for line in repair_lines(state, "claude-opus-5"))
def test_the_opposite_direction_is_a_different_and_louder_fault():
state, detail = grade_ceiling(reported_window(SONNET_4_5), 1_000_000)
assert state == "cap-above-model"
assert "400 prompt is too long" in detail
# And an aligned pair is not a finding at all.
assert grade_ceiling(reported_window(HAIKU), 200_000)[0] == "aligned"
def test_the_same_beta_header_is_two_findings_depending_on_the_model():
inert = grade_betas(reported_window(OPUS_5), ["context-1m-2025-08-07"])
assert [s for s, _ in inert] == ["inert-beta-header"]
assert "does nothing" in inert[0][1]
retired = grade_betas(reported_window(SONNET_4_5), ["context-1m-2025-08-07"])
assert [s for s, _ in retired] == ["retired-beta"]
assert "2026-04-30" in retired[0][1]
# An unrelated beta is not this note's business.
assert grade_betas(1_000_000, ["some-other-beta"]) == []
assert grade_betas(None, ["context-1m-2025-08-07"]) == []
def test_a_long_context_premium_branch_prices_something_that_is_free():
state, detail = grade_premium(reported_window(OPUS_5), True)
assert state == "phantom-premium"
assert "same per-token rate" in detail
assert grade_premium(reported_window(OPUS_5), False) is None
# On a 200k model there is no long-context branch to be wrong about.
assert grade_premium(reported_window(SONNET_4_5), True) is None
def test_one_stale_id_carries_several_findings_at_once():
rules = parse_rules({"claude-opus-5": {
"max_input_tokens": 200_000,
"beta_headers": "context-1m-2025-08-07",
"long_context_premium": True}})
states = [s for s, _ in audit(OPUS_5, rules["claude-opus-5"])]
assert states == ["capped-in-code", "inert-beta-header", "phantom-premium"]
# A clean model produces one quiet line and no findings.
clean = parse_rules({"claude-haiku-4-5-20251001": {"max_input_tokens": 200_000}})
assert [s for s, _ in audit(HAIKU, clean["claude-haiku-4-5-20251001"])] == \
["aligned"]
def test_model_ids_are_validated_before_they_reach_a_url():
assert valid_model_id("claude-opus-5") is True
assert valid_model_id("claude-haiku-4-5-20251001") is True
assert valid_model_id("../../organizations") is False
assert valid_model_id("claude opus 5") is False
assert valid_model_id("") is False
assert valid_model_id(None) is False
rules = parse_rules({"../../etc": {"max_input_tokens": 1},
"claude-opus-5": {"max_input_tokens": 200_000}})
assert list(rules) == ["claude-opus-5"]
def test_a_missing_window_is_not_a_window_of_zero():
assert reported_window({}) is None
assert reported_window({"max_input_tokens": 0}) is None
assert reported_window({"max_input_tokens": "1000000"}) == 1_000_000
assert reported_output(OPUS_5) == 128_000
assert shortfall(None, 200_000) is None
state, detail = grade_ceiling(None, 200_000)
assert state == "window-not-reported"
assert "no claim is made" in detail
def test_rules_default_safely_when_the_config_is_thin():
rules = parse_rules({"claude-opus-5": {}})
assert rules["claude-opus-5"] == {"cap": None, "betas": [], "premium": False}
assert audit(OPUS_5, rules["claude-opus-5"]) == []
assert parse_rules(None) == {}
assert parse_rules({"claude-opus-5": "not a dict"})["claude-opus-5"]["cap"] is None
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { audit, gradeBetas, gradeCeiling, gradePremium, parseRules,
repairLines, reportedOutput, reportedWindow, shortfall, validModelId }
from './anthropic-context-window-cap.mjs';
const OPUS_5 = { id: 'claude-opus-5', max_input_tokens: 1_000_000,
max_output_tokens: 128_000 };
const SONNET_4_5 = { id: 'claude-sonnet-4-5', max_input_tokens: 200_000,
max_output_tokens: 64_000 };
const HAIKU = { id: 'claude-haiku-4-5-20251001', max_input_tokens: 200_000,
max_output_tokens: 64_000 };
test('a million token window enforced at two hundred thousand', () => {
const rules = parseRules({ 'claude-opus-5': { max_input_tokens: 200_000 } });
const [state, detail] = gradeCeiling(reportedWindow(OPUS_5),
rules['claude-opus-5'].cap);
assert.equal(state, 'capped-in-code');
assert.match(detail, /800000 token\(s\) of window bought and unreachable/);
assert.equal(shortfall(1_000_000, 200_000), 800_000);
assert.ok(repairLines(state, 'claude-opus-5')
.some((line) => line.includes('raise the enforced ceiling')));
});
test('the opposite direction is a different and louder fault', () => {
const [state, detail] = gradeCeiling(reportedWindow(SONNET_4_5), 1_000_000);
assert.equal(state, 'cap-above-model');
assert.match(detail, /400 prompt is too long/);
assert.equal(gradeCeiling(reportedWindow(HAIKU), 200_000)[0], 'aligned');
});
test('the same beta header is two findings depending on the model', () => {
const inert = gradeBetas(reportedWindow(OPUS_5), ['context-1m-2025-08-07']);
assert.deepEqual(inert.map(([s]) => s), ['inert-beta-header']);
assert.match(inert[0][1], /does nothing/);
const retired = gradeBetas(reportedWindow(SONNET_4_5), ['context-1m-2025-08-07']);
assert.deepEqual(retired.map(([s]) => s), ['retired-beta']);
assert.match(retired[0][1], /2026-04-30/);
assert.deepEqual(gradeBetas(1_000_000, ['some-other-beta']), []);
assert.deepEqual(gradeBetas(null, ['context-1m-2025-08-07']), []);
});
test('a long context premium branch prices something that is free', () => {
const [state, detail] = gradePremium(reportedWindow(OPUS_5), true);
assert.equal(state, 'phantom-premium');
assert.match(detail, /same per-token rate/);
assert.equal(gradePremium(reportedWindow(OPUS_5), false), null);
assert.equal(gradePremium(reportedWindow(SONNET_4_5), true), null);
});
test('one stale id carries several findings at once', () => {
const rules = parseRules({ 'claude-opus-5': {
max_input_tokens: 200_000,
beta_headers: 'context-1m-2025-08-07',
long_context_premium: true } });
assert.deepEqual(audit(OPUS_5, rules['claude-opus-5']).map(([s]) => s),
['capped-in-code', 'inert-beta-header', 'phantom-premium']);
const clean = parseRules({ 'claude-haiku-4-5-20251001': { max_input_tokens: 200_000 } });
assert.deepEqual(audit(HAIKU, clean['claude-haiku-4-5-20251001']).map(([s]) => s),
['aligned']);
});
test('model ids are validated before they reach a url', () => {
assert.equal(validModelId('claude-opus-5'), true);
assert.equal(validModelId('claude-haiku-4-5-20251001'), true);
assert.equal(validModelId('../../organizations'), false);
assert.equal(validModelId('claude opus 5'), false);
assert.equal(validModelId(''), false);
assert.equal(validModelId(null), false);
const rules = parseRules({ '../../etc': { max_input_tokens: 1 },
'claude-opus-5': { max_input_tokens: 200_000 } });
assert.deepEqual(Object.keys(rules), ['claude-opus-5']);
});
test('a missing window is not a window of zero', () => {
assert.equal(reportedWindow({}), null);
assert.equal(reportedWindow({ max_input_tokens: 0 }), null);
assert.equal(reportedWindow({ max_input_tokens: '1000000' }), 1_000_000);
assert.equal(reportedOutput(OPUS_5), 128_000);
assert.equal(shortfall(null, 200_000), null);
const [state, detail] = gradeCeiling(null, 200_000);
assert.equal(state, 'window-not-reported');
assert.match(detail, /no claim is made/);
});
test('rules default safely when the config is thin', () => {
const rules = parseRules({ 'claude-opus-5': {} });
assert.deepEqual(rules['claude-opus-5'], { cap: null, betas: [], premium: false });
assert.deepEqual(audit(OPUS_5, rules['claude-opus-5']), []);
assert.deepEqual(parseRules(null), {});
assert.equal(parseRules({ 'claude-opus-5': 'not a dict' })['claude-opus-5'].cap,
null);
});
FAQ
Why does the script need a config file instead of finding the ceiling itself?
Because no endpoint on either API can see your source. The provider knows what the model supports; only your repository knows what your code enforces, which header values the request path still attaches, and whether a long-context price branch still exists. Half of this comparison has to be declared, and the script says so rather than presenting a declaration as a discovery. Writing that file down is usually the first time the four facts are looked at together.
Is the 1M context window still behind a beta header?
No, and that is the whole point. On Claude Opus 5, Opus 4.8, 4.7 and 4.6, Sonnet 5 and 4.6, Fable 5, Mythos 5 and Mythos Preview, the 1M window is the default: no header, standard pricing across the full window, prompt caching and batch discounts at standard rates, and no separate long-context rate limits. The header only ever applied to Sonnet 4.5 and Sonnet 4, and on those it was retired on 30 April 2026.
The header is still accepted, so surely it still works?
Acceptance is not effect. The beta name is still recognised, so a request carrying it does not fail, which is exactly what makes it convincing. On a model that already defaults to 1M the header changes nothing, and on Sonnet 4.5 or Sonnet 4 it changes nothing either, but there anything over 200k now returns 400 invalid_request_error with prompt is too long. That is why this script does not probe the header at all: a 200 back from a probe would be evidence of nothing.
Does a very long request cost more per token?
Not on current models. A 900k-token request bills at the same per-token rate as a 9k one, and the dedicated long-context rate limits were removed along with the premium, so standard account limits apply at every context length. The belief that crossing 200k triggers premium pricing was true of the retired beta and is not true now, which is why a surviving price or throttle branch is graded as a finding rather than as prudence.
What about max_tokens, since 1M models cap output at 128k?
That is a real ceiling and a separate one, so it has its own note. The model object carries max_output_tokens alongside max_input_tokens, and a configured max_tokens above it fails differently and is repaired differently. This script prints the reported output ceiling as context and deliberately does not grade it, because duplicating a check in two places is how two places end up disagreeing.
Related field notes
- How much traffic actually crosses 200k, measured in the usage report
- One payload counted against the window before you send it
- The output ceiling, which is a different number and a different note
Sources
Every figure in this note is traced to one of these. Prices are list rates and change — check them for your own region before acting.
- Context windows — Claude Docs
- Models — Claude API reference
- Pricing — Claude Docs
- Models overview — Claude Docs
If your setup is misbehaving in a way this note does not cover, message me on LinkedIn with what you are seeing.