Context Window Planner — JavaScript source
Paste your system prompt, docs, and history — see how they fill any model's context window, with overflow warnings and output headroom.
This is the JavaScript implementation — the same logic the interactive tool runs, in a shareable, citable form.
/**
* Context Window Planner - plan labeled prompt sections against a model's
* context window.
*
* Language: JavaScript (ES2020+, ES module; runs unmodified in Node 16+
* and modern browsers)
* Source: CosmoDev polyglot showcase port of the Context Window Planner
* tool, ported from src/lib/contextPlanner.ts (the canonical
* TypeScript implementation).
* Live page: https://dev.cosmolabs.org/tools/context-window-planner
* License: display source - part of CosmoDev's tool pages.
*
* Design goals:
* - Pure + deterministic; never throws.
* - Functionally equivalent to the TS reference: same inputs -> same outputs.
* - Self-contained: no imports, no snapshot fetch.
*
* Port notes: the TS lib delegates to two siblings — `estimateTokens` from
* src/lib/tokenEstimator.ts and `fitsWindow` from src/lib/ai/models.ts (which
* defaults to the bundled pricing snapshot, src/data/ai-models.json). A
* dependency-free port cannot load that file, so:
* - the estimator is inlined below in the exact form the planner uses it
* (`estimateTokens(text).tokens`, auto content type — the full heuristic
* lives in the token-estimator port), and
* - window math is inlined from fitsWindow() and `models` is an explicit
* parameter (defaulting to an empty list), never re-derived.
*/
/**
* One labeled block of the prompt (system / docs / history / ...).
*
* @typedef {Object} PlanSection
* @property {string} label
* @property {string} text
*/
/**
* The subset of the TS `AiModel` record the planner reads. Production code
* passes the full snapshot entry; only these fields influence the plan.
*
* @typedef {Object} Model
* @property {string} id
* @property {number} contextWindow Total context window in tokens.
* @property {number} maxOutput The model's output cap (informational).
*/
/**
* Result of planWindow. Field-for-field twin of the TS `WindowPlan`.
*
* @typedef {Object} WindowPlan
* @property {string} id The model id planned against.
* @property {number} inputTokens Sum of per-section token estimates.
* @property {number} contextWindow The model's context window.
* @property {number} free Context tokens left; negative on overflow.
* @property {boolean} fits Raw fit: free >= 0.
* @property {boolean} outputReserveOk Room for the reserve: free >= reserve.
* @property {number} maxOutput The model's output cap (informational).
*/
/**
* Result of the fit check. Mirrors `WindowFit` from src/lib/ai/models.ts.
*
* @typedef {Object} WindowFit
* @property {Model} model
* @property {number} contextWindow
* @property {number} tokens
* @property {number} free
* @property {boolean} fits
*/
/** Average characters per token, by content type. Mirrors CHARS_PER_TOKEN
* in src/lib/tokenEstimator.ts. */
const CHARS_PER_TOKEN = { prose: 4, code: 3.5, json: 3, cjk: 1.5 };
const CJK_RE = /[一-鿿-ヿ가-]/;
const CODE_SYMBOL_RE = /[{}();=<>\[\]#]/g;
/** Sample table for standalone use (mirrors the shared test fixtures).
* Production code passes the model snapshot instead. */
export const SAMPLE_MODELS = [
{ id: 'alpha-mini', contextWindow: 200_000, maxOutput: 10_000 },
{ id: 'beta-pro', contextWindow: 1_000_000, maxOutput: 10_000 },
{ id: 'gamma-open', contextWindow: 100_000, maxOutput: 10_000 },
];
/**
* Classify a single line by its shape. Order: json, cjk, code, prose.
* Inlined from detectLineType() in src/lib/tokenEstimator.ts.
*
* @param {string} line
* @returns {'prose' | 'code' | 'json' | 'cjk'}
*/
function detectLineType(line) {
const trimmed = line.trim();
// JSON-ish: opens like a JSON fragment AND carries a separator.
if ((trimmed.startsWith('{') || trimmed.startsWith('}') || trimmed.startsWith('[') || trimmed.startsWith('"')) && (line.includes(':') || line.includes(','))) {
return 'json';
}
// CJK ideographs / kana / Hangul pack roughly one token per 1.5 chars.
if (CJK_RE.test(line)) return 'cjk';
// Code: symbol-dense, or a statement terminator / block opener at EOL.
const density = (line.match(CODE_SYMBOL_RE) ?? []).length / line.length;
if (density > 0.08 || trimmed.endsWith(';') || trimmed.endsWith('{') || trimmed.endsWith('}')) {
return 'code';
}
return 'prose';
}
/** Whole-text JSON gate (isValidJson in the estimator lib). */
function isValidJson(text) {
if (!text.trim()) return false;
try {
JSON.parse(text);
return true;
} catch {
return false;
}
}
/**
* Token count of `text` under auto content detection — exactly the slice of
* estimateTokens() the planner consumes (`.tokens`). Per non-empty line:
* max(1, round(utf16Length / CHARS_PER_TOKEN[type])); framing tokens are the
* caller's job.
*
* @param {string} text
* @returns {number}
*/
export function estimateTokens(text) {
// AUTO + whole-text JSON: a document that parses as JSON is json all the
// way down — json's 3 chars/token rate applies to every line.
const wholeTextJson = isValidJson(text);
const nonEmpty = text.split(/\r?\n/).filter((line) => line.trim() !== '');
let tokens = 0;
for (const line of nonEmpty) {
const type = wholeTextJson ? 'json' : detectLineType(line);
tokens += Math.max(1, Math.round(line.length / CHARS_PER_TOKEN[type]));
}
return tokens;
}
/**
* Sum of per-section token estimates (framing tokens are the caller's job).
* Mirrors inputTokenTotal() in the TS lib.
*
* @param {PlanSection[]} sections
* @returns {number}
*/
export function inputTokenTotal(sections) {
return sections.reduce((n, s) => n + estimateTokens(s.text), 0);
}
/**
* Fit check for one request against a model's context window. Inlined from
* fitsWindow() in src/lib/ai/models.ts — returns undefined for an unknown id.
*
* @param {string} id
* @param {number} tokens
* @param {Model[]} [models=[]]
* @returns {WindowFit | undefined}
*/
export function fitsWindow(id, tokens, models = []) {
const m = models.find((m) => m.id === id);
if (m === undefined) return undefined;
const free = m.contextWindow - tokens;
return { model: m, contextWindow: m.contextWindow, tokens, free, fits: free >= 0 };
}
/**
* Plan one section set against one model's context window. Returns undefined
* for an unknown model id (window math is fitsWindow's, never re-derived).
* Mirrors planWindow() in the TS lib.
*
* @param {PlanSection[]} sections
* @param {string} modelId
* @param {number} [outputReserve=0]
* @param {Model[]} [models=[]]
* @returns {WindowPlan | undefined}
*/
export function planWindow(sections, modelId, outputReserve = 0, models = []) {
const inputTokens = inputTokenTotal(sections);
const fit = fitsWindow(modelId, inputTokens, models);
if (fit === undefined) return undefined;
return {
id: modelId,
inputTokens,
contextWindow: fit.contextWindow,
free: fit.free,
fits: fit.fits,
outputReserveOk: fit.free >= outputReserve,
maxOutput: fit.model.maxOutput,
};
}
/**
* Plan against several models; unknown ids are dropped from the result.
* Mirrors planAll() in the TS lib.
*
* @param {PlanSection[]} sections
* @param {string[]} modelIds
* @param {number} [outputReserve=0]
* @param {Model[]} [models=[]]
* @returns {WindowPlan[]}
*/
export function planAll(sections, modelIds, outputReserve = 0, models = []) {
return modelIds
.map((id) => planWindow(sections, id, outputReserve, models))
.filter((p) => p !== undefined);
}
Also available in 13 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →