Context Window Planner — C# 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 C# 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: C# (C# 12 / .NET 8, base class library only)
// Source: CosmoDev polyglot showcase port of the Context Window Planner
// tool, ported from src/lib/contextPlanner.ts (the canonical
// TypeScript implementation).
// Live at: https://dev.cosmolabs.org/tools/context-window-planner
// License: display source — part of CosmoDev's polyglot tool pages.
//
// Design goals:
// - Pure + deterministic; never throws (public API returns plain values).
// - Functionally equivalent to the TS reference: same inputs -> same outputs.
// - Self-contained: BCL only. (System.Text.Json would be the stdlib
// equivalent of serde_json; this port instead ships the same small strict
// recursive-descent validator the other polyglot siblings use, so the
// JSON grammar matches JSON.parse exactly rather than approximately.)
//
// 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),
// window math is inlined from `fitsWindow` and `models` is an explicit
// parameter, never re-derived.
//
// Faithfulness notes (the places C#'s runtime silently differs from JS):
// - Length: TS's `String.length` counts UTF-16 code units — and so does
// C#'s `string.Length`, so no helper is needed; astral-plane characters
// (emoji, rare CJK ext-B ideographs) already count as 2 on both sides.
// - Rounding: `MidpointRounding.AwayFromZero` on non-negative values is
// exactly JS `Math.round` (halfway cases up); the helper states it.
using System;
using System.Collections.Generic;
using System.Globalization;
namespace CosmoDev.ContextWindowPlanner;
/// One labeled block of the prompt (system / docs / history / ...).
/// Mirrors the TS <c>PlanSection</c> interface.
public sealed record PlanSection(string Label, string Text);
/// The subset of the TS <c>AiModel</c> record the planner reads.
/// Production code passes the full snapshot entry; only these fields
/// influence the plan.
public sealed record Model(string Id, long ContextWindow, long MaxOutput);
/// Result of <see cref="Planner.PlanWindow"/>. Field-for-field twin of the
/// TS <c>WindowPlan</c> interface.
public sealed record WindowPlan(
string Id,
long InputTokens,
long ContextWindow,
long Free,
bool Fits,
bool OutputReserveOk,
long MaxOutput);
public static class Planner
{
/// Sample table for standalone use (mirrors the shared test fixtures).
/// Production code passes the model snapshot instead.
public static readonly IReadOnlyList<Model> SampleModels = new[] {
new Model("alpha-mini", 200_000, 10_000),
new Model("beta-pro", 1_000_000, 10_000),
new Model("gamma-open", 100_000, 10_000),
};
/// Content classification of a single line. The planner only needs each
/// type's chars-per-token rate (mirrors <c>CHARS_PER_TOKEN</c> in
/// src/lib/tokenEstimator.ts: prose 4, code 3.5, json 3, cjk 1.5).
private enum ContentType { Prose, Code, Json, Cjk }
private static double CharsPerToken(ContentType t) => t switch
{
ContentType.Prose => 4.0,
ContentType.Code => 3.5,
ContentType.Json => 3.0,
ContentType.Cjk => 1.5,
_ => 4.0,
};
/// Reports whether <paramref name="s"/> contains a CJK ideograph
/// (U+4E00–U+9FFF), kana (U+3040–U+30FF), or a Hangul syllable
/// (U+AC00–U+D7AF). Mirrors <c>CJK_RE</c> in the TS lib.
private static bool HasCjk(string s)
{
foreach (char c in s)
{
if ((c >= '一' && c <= '鿿') ||
(c >= '' && c <= 'ヿ') ||
(c >= '가' && c <= ''))
return true;
}
return false;
}
/// Reports whether <paramref name="c"/> is one of the code-flavored
/// symbols counted by <c>CODE_SYMBOL_RE</c> (<c>{}();<>[]#</c>).
private static bool IsCodeSymbol(char c) =>
c is '{' or '}' or '(' or ')' or ';' or '=' or '<' or '>' or '[' or ']' or '#';
/// JS <c>Math.round</c>: halfway cases round up
/// (<c>AwayFromZero</c> over the non-negative inputs used here).
private static long JsRound(double x) =>
(long)Math.Round(x, MidpointRounding.AwayFromZero);
/// Splits <paramref name="text"/> on LF or CRLF, mirroring
/// <c>text.split(/\r?\n/)</c>: strip the optional CR that belongs to the
/// newline, then split on LF. A lone CR is NOT a line break.
private static string[] SplitLines(string text) => text.Split('\n');
/// Classifies a single line by its shape. Order: json, cjk, code, prose.
/// Inlined from <c>detectLineType()</c> in src/lib/tokenEstimator.ts.
private static ContentType DetectLineType(string line)
{
string trimmed = line.Trim();
// JSON-ish: opens like a JSON fragment AND carries a separator.
bool startsJsonish = trimmed.StartsWith('{') || trimmed.StartsWith('}') ||
trimmed.StartsWith('[') || trimmed.StartsWith('"');
if (startsJsonish && (line.Contains(':') || line.Contains(',')))
return ContentType.Json;
// CJK ideographs / kana / Hangul pack roughly one token per 1.5 chars.
if (HasCjk(line))
return ContentType.Cjk;
// Code: symbol-dense, or a statement terminator / block opener at EOL.
int length = line.Length; // UTF-16 code units, same unit as TS
int symbols = 0;
foreach (char c in line)
if (IsCodeSymbol(c)) symbols++;
double density = length > 0 ? (double)symbols / length : 0.0;
if (density > 0.08 || trimmed.EndsWith(";") || trimmed.EndsWith("{") ||
trimmed.EndsWith("}"))
return ContentType.Code;
return ContentType.Prose;
}
/// A strict JSON syntax validator — the exact grammar <c>JSON.parse</c>
/// accepts, walked with a cursor. Same shape as the Rust/C/C++
/// siblings' validators, so whole-text JSON detection behaves
/// identically across every port.
private sealed class JsonParser
{
private readonly string s;
private int pos;
internal JsonParser(string text) => s = text;
/// value := ws* (object | array | string | number | 'true' | 'false' | 'null') ws*
internal bool Value()
{
SkipWs();
return Peek() switch
{
'{' => Object(),
'[' => Array(),
'"' => String(),
'-' or (>= '0' and <= '9') => Number(),
't' => Literal("true"),
'f' => Literal("false"),
'n' => Literal("null"),
_ => false,
};
}
internal bool AtEnd()
{
SkipWs();
return pos == s.Length; // reject trailing garbage
}
private void SkipWs()
{
while (pos < s.Length &&
(s[pos] == ' ' || s[pos] == '\t' || s[pos] == '\n' || s[pos] == '\r'))
pos++;
}
private char Peek() => pos < s.Length ? s[pos] : '\0';
private bool Eat(char b)
{
if (Peek() == b && pos < s.Length) { pos++; return true; }
return false;
}
private bool Literal(string lit)
{
if (s.AsSpan(pos).StartsWith(lit))
{
pos += lit.Length;
return true;
}
return false;
}
/// object := '{' ws* (string ws* ':' value (ws* ',' ...)*)? ws* '}'
private bool Object()
{
if (!Eat('{')) return false;
SkipWs();
if (Eat('}')) return true;
while (true)
{
if (!String()) return false;
SkipWs();
if (!Eat(':')) return false;
if (!Value()) return false;
SkipWs();
if (Eat(',')) SkipWs();
else return Eat('}');
}
}
/// array := '[' ws* (value (ws* ',' ws* value)*)? ws* ']'
private bool Array()
{
if (!Eat('[')) return false;
SkipWs();
if (Eat(']')) return true;
while (true)
{
if (!Value()) return false;
SkipWs();
if (Eat(',')) SkipWs();
else return Eat(']');
}
}
/// string := '"' (escape | any char >= 0x20)* '"'
/// escape := '\' ('"' | '/' | '\' | 'b' | 'f' | 'n' | 'r' | 't' | 'u' hex4)
private bool String()
{
if (!Eat('"')) return false;
while (pos < s.Length)
{
char c = s[pos];
if (c == '"') { pos++; return true; }
if (c == '\\')
{
pos++;
if (pos >= s.Length) return false;
char esc = s[pos++];
switch (esc)
{
case '"' or '/' or '\\' or 'b' or 'f' or 'n' or 'r' or 't':
break;
case 'u':
for (int i = 0; i < 4; i++)
{
char h = Peek();
bool hex = (h >= '0' && h <= '9') ||
(h >= 'a' && h <= 'f') ||
(h >= 'A' && h <= 'F');
if (!hex) return false;
pos++;
}
break;
default:
return false;
}
}
else if (c < 0x20)
{
return false; // raw control characters not allowed in strings
}
else
{
pos++;
}
}
return false; // unterminated string
}
/// number := '-'? int frac? exp? — no leading zeros, like JSON.parse.
private bool Number()
{
Eat('-');
if (Peek() == '0') pos++;
else if (Peek() >= '1' && Peek() <= '9')
{
while (Peek() >= '0' && Peek() <= '9') pos++;
}
else return false;
if (Peek() == '.')
{
pos++;
int digits = 0;
while (Peek() >= '0' && Peek() <= '9') { pos++; digits++; }
if (digits == 0) return false;
}
if (Peek() == 'e' || Peek() == 'E')
{
pos++;
if (Peek() == '+' || Peek() == '-') pos++;
int digits = 0;
while (Peek() >= '0' && Peek() <= '9') { pos++; digits++; }
if (digits == 0) return false;
}
return true;
}
}
/// Whole-text JSON gate: a document that parses as JSON is json all the
/// way down. Mirrors <c>isValidJson()</c> (<c>JSON.parse</c> in a
/// try/catch); empty/whitespace text is not.
private static bool IsValidJson(string text)
{
if (string.IsNullOrWhiteSpace(text)) return false;
var p = new JsonParser(text);
return p.Value() && p.AtEnd();
}
/// Token count of <paramref name="text"/> under auto content detection —
/// exactly the slice of <c>estimateTokens()</c> the planner consumes
/// (<c>.tokens</c>): per non-empty line,
/// <c>max(1, round(utf16Len / charsPerToken))</c>. Framing tokens are the
/// caller's job.
private static long EstimateTokens(string text)
{
// AUTO + whole-text JSON: json's 3 chars/token rate applies to every
// line, not just the reported content type.
bool wholeTextJson = IsValidJson(text);
long tokens = 0;
foreach (string rawLine in SplitLines(text))
{
string line = rawLine.EndsWith("\r") ? rawLine[..^1] : rawLine;
if (line.Trim().Length == 0) continue;
ContentType t = wholeTextJson ? ContentType.Json : DetectLineType(line);
long lineTokens = JsRound(line.Length / CharsPerToken(t));
tokens += Math.Max(1, lineTokens);
}
return tokens;
}
/// Sum of per-section token estimates (framing tokens are the caller's
/// job). Mirrors <c>inputTokenTotal()</c> in the TS lib.
public static long InputTokenTotal(IEnumerable<PlanSection> sections)
{
long total = 0;
foreach (PlanSection s in sections) total += EstimateTokens(s.Text);
return total;
}
/// Plan one section set against one model's context window. Returns
/// <c>null</c> for an unknown model id (window math is
/// <c>fitsWindow</c>'s, never re-derived). Mirrors <c>planWindow()</c>
/// in the TS lib.
public static WindowPlan? PlanWindow(
IReadOnlyList<PlanSection> sections, string modelId,
long outputReserve = 0, IReadOnlyList<Model>? models = null)
{
models ??= Array.Empty<Model>();
long inputTokens = InputTokenTotal(sections);
// Fit check inlined from fitsWindow() in src/lib/ai/models.ts.
Model? m = null;
foreach (Model cand in models)
if (cand.Id == modelId) { m = cand; break; }
if (m is null) return null;
long free = m.ContextWindow - inputTokens;
return new WindowPlan(
Id: modelId,
InputTokens: inputTokens,
ContextWindow: m.ContextWindow,
Free: free,
Fits: free >= 0,
OutputReserveOk: free >= outputReserve,
MaxOutput: m.MaxOutput);
}
/// Plan against several models; unknown ids are dropped from the result.
/// Mirrors <c>planAll()</c> in the TS lib.
public static List<WindowPlan> PlanAll(
IReadOnlyList<PlanSection> sections, IReadOnlyList<string> modelIds,
long outputReserve = 0, IReadOnlyList<Model>? models = null)
{
var plans = new List<WindowPlan>();
foreach (string id in modelIds)
{
WindowPlan? plan = PlanWindow(sections, id, outputReserve, models);
if (plan is not null) plans.Add(plan);
}
return plans;
}
}
// ---------- tests (showcase-only; the canonical suite lives in src/lib) ----------
// Run: dotnet run --property:DefineConstants=CWP_TEST
#if CWP_TEST
public static class PlannerTests
{
// A 1600-char single line of 'a' is pure prose: 1600 / 4 = 400 tokens.
private static readonly string LineA = new('a', 1600);
private static PlanSection[] TwoSections() =>
new[] { new PlanSection("sys", LineA), new PlanSection("docs", LineA) };
public static int Run()
{
// input totals
Assert(Planner.InputTokenTotal(TwoSections()) == 800);
Assert(Planner.InputTokenTotal(Array.Empty<PlanSection>()) == 0);
Assert(Planner.InputTokenTotal(new[] { new PlanSection("sys", "") }) == 0);
// plans two 400-token sections against beta-pro
WindowPlan p = Planner.PlanWindow(TwoSections(), "beta-pro", 0, Planner.SampleModels)!;
Assert(p.Id == "beta-pro");
Assert(p.InputTokens == 800);
Assert(p.ContextWindow == 1_000_000);
Assert(p.Free == 999_200);
Assert(p.Fits);
Assert(p.OutputReserveOk);
Assert(p.MaxOutput == 10_000);
// reserve larger than free leaves raw fit true
p = Planner.PlanWindow(TwoSections(), "beta-pro", 1_000_000, Planner.SampleModels)!;
Assert(p.Fits);
Assert(!p.OutputReserveOk);
// reserve exactly equal to free is ok
p = Planner.PlanWindow(TwoSections(), "beta-pro", 999_200, Planner.SampleModels)!;
Assert(p.OutputReserveOk);
// smaller window leaves 199,200 free
p = Planner.PlanWindow(TwoSections(), "alpha-mini", 0, Planner.SampleModels)!;
Assert(p.ContextWindow == 200_000);
Assert(p.Free == 199_200);
Assert(p.Fits);
// unknown model id returns null
Assert(Planner.PlanWindow(TwoSections(), "ghost", 0, Planner.SampleModels) is null);
// no sections: full window free
p = Planner.PlanWindow(Array.Empty<PlanSection>(), "beta-pro", 0, Planner.SampleModels)!;
Assert(p.InputTokens == 0);
Assert(p.Free == 1_000_000);
Assert(p.Fits);
// overflow: fits false, reserve false
var big = new[] { new PlanSection("big", new string('z', 4_400_000)) };
p = Planner.PlanWindow(big, "beta-pro", 0, Planner.SampleModels)!;
Assert(p.InputTokens == 1_100_000);
Assert(p.Free == -100_000);
Assert(!p.Fits);
Assert(!p.OutputReserveOk);
// plan_all drops unknown ids and keeps order
var plans = Planner.PlanAll(TwoSections(),
new[] { "beta-pro", "alpha-mini", "ghost" }, 0, Planner.SampleModels);
Assert(plans.Count == 2);
Assert(plans[0].Id == "beta-pro");
Assert(plans[1].Id == "alpha-mini");
Assert(plans[1].Free == 199_200);
Assert(Planner.PlanAll(TwoSections(), Array.Empty<string>(), 0,
Planner.SampleModels).Count == 0);
Console.WriteLine("context-window-planner (C#): all tests passed");
return 0;
}
private static void Assert(bool cond) =>
System.Diagnostics.Debug.Assert(cond);
}
#endif
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 →