Context Window Planner — Swift 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 Swift 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: Swift (Swift 5.9, standard 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 traps (public API returns plain values).
// - Functionally equivalent to the TS reference: same inputs -> same outputs.
// - Self-contained: the Swift stdlib only (Foundation's JSONSerialization
// is the framework 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 Swift's stdlib silently differs from JS):
// - Length: TS's `String.length` counts UTF-16 code units (an astral-plane
// character — emoji, rare CJK ext-B ideographs — counts as 2). Swift's
// `String.count` counts Characters (grapheme clusters), so line
// arithmetic goes through `utf16Len` (`s.utf16.count`) to count the same
// unit.
// - Rounding: `jsRound` is `floor(x + 0.5)` — JS `Math.round` rounds
// halfway cases up; `.toNearestOrAwayFromZero` matches only over
// non-negative inputs, so the helper pins the exact formula.
/// Namespace for the planner's pure logic — no state, only functions and
/// value types.
enum ContextWindowPlanner {
/// One labeled block of the prompt (system / docs / history / ...).
/// Mirrors the TS `PlanSection` interface.
struct PlanSection: Equatable {
/// Section label, e.g. "system" or "docs".
let label: String
/// The section's raw text.
let text: String
}
/// Convenience constructor mirroring the TS object literal `{ label, text }`.
static func sec(_ label: String, _ text: String) -> PlanSection {
PlanSection(label: label, text: text)
}
/// The subset of the TS `AiModel` record the planner reads. Production
/// code passes the full snapshot entry; only these fields influence the
/// plan.
struct Model: Equatable {
/// Model id, e.g. "beta-pro".
let id: String
/// Total context window in tokens.
let contextWindow: Int
/// The model's output cap (informational).
let maxOutput: Int
}
/// Sample table for standalone use (mirrors the shared test fixtures).
/// Production code passes the model snapshot instead.
static let sampleModels: [Model] = [
Model(id: "alpha-mini", contextWindow: 200_000, maxOutput: 10_000),
Model(id: "beta-pro", contextWindow: 1_000_000, maxOutput: 10_000),
Model(id: "gamma-open", contextWindow: 100_000, maxOutput: 10_000),
]
/// Result of `planWindow`. Field-for-field twin of the TS `WindowPlan`
/// interface.
struct WindowPlan: Equatable {
/// The model id planned against.
let id: String
/// Sum of per-section token estimates.
let inputTokens: Int
/// The model's context window.
let contextWindow: Int
/// Context tokens left after the request; negative on overflow.
let free: Int
/// Raw fit: free >= 0.
let fits: Bool
/// Room for the output reserve: free >= reserve.
let outputReserveOk: Bool
/// The model's output cap (informational).
let maxOutput: Int
}
// MARK: - estimator internals
/// Content classification of a single line. The planner only needs each
/// type's chars-per-token rate (mirrors `CHARS_PER_TOKEN` in
/// src/lib/tokenEstimator.ts: prose 4, code 3.5, json 3, cjk 1.5).
private enum ContentType {
case prose, code, json, cjk
var charsPerToken: Double {
switch self {
case .prose: return 4.0
case .code: return 3.5
case .json: return 3.0
case .cjk: return 1.5
}
}
}
/// Length of `s` in UTF-16 code units — the unit TS's `String.length`
/// counts. BMP code points are one unit, astral-plane ones two.
private static func utf16Len(_ s: Substring) -> Int {
s.utf16.count
}
/// Reports whether `s` contains a CJK ideograph (U+4E00–U+9FFF), kana
/// (U+3040–U+30FF), or a Hangul syllable (U+AC00–U+D7AF). Mirrors
/// `CJK_RE` in the TS lib.
private static func hasCjk(_ s: Substring) -> Bool {
s.unicodeScalars.contains { c in
(0x4E00...0x9FFF).contains(c.value) ||
(0x3040...0x30FF).contains(c.value) ||
(0xAC00...0xD7AF).contains(c.value)
}
}
/// Reports whether `c` is one of the code-flavored symbols counted by
/// `CODE_SYMBOL_RE` (`{}();=<>[]#`).
private static func isCodeSymbol(_ c: Character) -> Bool {
"{}();=<>[]#".contains(c)
}
/// JS `Math.round`: halfway cases round up (`floor(x + 0.5)`).
private static func jsRound(_ x: Double) -> Int {
Int((x + 0.5).rounded(.down))
}
/// Splits `text` on LF or CRLF, mirroring `text.split(/\r?\n/)`: strip
/// the optional CR that belongs to the newline, then split on LF. A lone
/// CR is NOT a line break.
private static func splitLines(_ text: String) -> [Substring] {
text.split(separator: "\n", omittingEmptySubsequences: false)
.map { line in line.hasSuffix("\r") ? line.dropLast() : line }
}
/// Classifies a single line by its shape. Order: json, cjk, code, prose.
/// Inlined from `detectLineType()` in src/lib/tokenEstimator.ts.
private static func detectLineType(_ line: Substring) -> ContentType {
let trimmed = line.trimmingWhitespace
// JSON-ish: opens like a JSON fragment AND carries a separator.
let first = trimmed.first
let startsJsonish = first == "{" || first == "}" || first == "[" || first == "\""
if startsJsonish, line.contains(":") || line.contains(",") {
return .json
}
// CJK ideographs / kana / Hangul pack roughly one token per 1.5 chars.
if hasCjk(line) {
return .cjk
}
// Code: symbol-dense, or a statement terminator / block opener at EOL.
let length = utf16Len(line)
let symbols = line.filter(isCodeSymbol).count
let density = length > 0 ? Double(symbols) / Double(length) : 0.0
if density > 0.08 || trimmed.hasSuffix(";") || trimmed.hasSuffix("{") ||
trimmed.hasSuffix("}") {
return .code
}
return .prose
}
/// A strict JSON syntax validator — the exact grammar `JSON.parse`
/// accepts, walked with a cursor. Same shape as the Rust/C/C++/C#/Java
/// siblings' validators, so whole-text JSON detection behaves
/// identically across every port.
private struct JsonParser {
let scalars: [Unicode.Scalar]
var pos = 0
init(_ text: String) {
scalars = Array(text.unicodeScalars)
}
/// value := ws* (object | array | string | number | 'true' | 'false' | 'null') ws*
mutating func value() -> Bool {
skipWs()
guard pos < scalars.count else { return false }
switch scalars[pos] {
case "{": return object()
case "[": return array()
case "\"": return string()
case "-", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9":
return number()
case "t": return literal("true")
case "f": return literal("false")
case "n": return literal("null")
default: return false
}
}
mutating func atEnd() -> Bool {
skipWs()
return pos == scalars.count // reject trailing garbage
}
private mutating func skipWs() {
while pos < scalars.count,
scalars[pos] == " " || scalars[pos] == "\t" ||
scalars[pos] == "\n" || scalars[pos] == "\r" {
pos += 1
}
}
private func peek() -> Unicode.Scalar? {
pos < scalars.count ? scalars[pos] : nil
}
private mutating func eat(_ b: Unicode.Scalar) -> Bool {
if pos < scalars.count && scalars[pos] == b { pos += 1; return true }
return false
}
private mutating func literal(_ lit: String) -> Bool {
let l = Array(lit.unicodeScalars)
guard scalars.count - pos >= l.count else { return false }
for (i, c) in l.enumerated() where scalars[pos + i] != c { return false }
pos += l.count
return true
}
/// object := '{' ws* (string ws* ':' value (ws* ',' ...)*)? ws* '}'
private mutating func object() -> Bool {
guard eat("{") else { return false }
skipWs()
if eat("}") { return true }
while true {
guard string() else { return false }
skipWs()
guard eat(":") else { return false }
guard value() else { return false }
skipWs()
if eat(",") { skipWs() }
else { return eat("}") }
}
}
/// array := '[' ws* (value (ws* ',' ws* value)*)? ws* ']'
private mutating func array() -> Bool {
guard eat("[") else { return false }
skipWs()
if eat("]") { return true }
while true {
guard value() else { return false }
skipWs()
if eat(",") { skipWs() }
else { return eat("]") }
}
}
/// string := '"' (escape | any scalar >= 0x20)* '"'
/// escape := '\' ('"' | '/' | '\' | 'b' | 'f' | 'n' | 'r' | 't' | 'u' hex4)
private mutating func string() -> Bool {
guard eat("\"") else { return false }
while pos < scalars.count {
let c = scalars[pos]
if c == "\"" { pos += 1; return true }
if c == "\\" {
pos += 1
guard pos < scalars.count else { return false }
let esc = scalars[pos]
pos += 1
switch esc {
case "\"", "/", "\\", "b", "f", "n", "r", "t":
break
case "u":
for _ in 0..<4 {
guard let h = peek(),
("0"..."9").contains(h) || ("a"..."f").contains(h) ||
("A"..."F").contains(h) else { return false }
pos += 1
}
default:
return false
}
} else if c.value < 0x20 {
return false // raw control characters not allowed in strings
} else {
pos += 1
}
}
return false // unterminated string
}
/// number := '-'? int frac? exp? — no leading zeros, like JSON.parse.
private mutating func number() -> Bool {
_ = eat("-")
switch peek() {
case "0": pos += 1
case .some(let c) where ("1"..."9").contains(c):
while let p = peek(), ("0"..."9").contains(p) { pos += 1 }
default: return false
}
if peek() == "." {
pos += 1
var digits = 0
while let p = peek(), ("0"..."9").contains(p) { pos += 1; digits += 1 }
guard digits > 0 else { return false }
}
if peek() == "e" || peek() == "E" {
pos += 1
if peek() == "+" || peek() == "-" { pos += 1 }
var digits = 0
while let p = peek(), ("0"..."9").contains(p) { pos += 1; digits += 1 }
guard digits > 0 else { return false }
}
return true
}
}
/// Whole-text JSON gate: a document that parses as JSON is json all the
/// way down. Mirrors `isValidJson()` (`JSON.parse` in a try/catch);
/// empty/whitespace text is not.
static func isValidJson(_ text: String) -> Bool {
if text.trimmingWhitespace.isEmpty { return false }
var p = JsonParser(text)
return p.value() && p.atEnd()
}
/// Token count of `text` under auto content detection — exactly the
/// slice of `estimateTokens()` the planner consumes (`.tokens`): per
/// non-empty line, `max(1, round(utf16Len / charsPerToken))`. Framing
/// tokens are the caller's job.
static func estimateTokens(_ text: String) -> Int {
// AUTO + whole-text JSON: json's 3 chars/token rate applies to every
// line, not just the reported content type.
let wholeTextJson = isValidJson(text)
var tokens = 0
for line in splitLines(text) {
if line.trimmingWhitespace.isEmpty { continue }
let t = wholeTextJson ? ContentType.json : detectLineType(line)
tokens += max(1, jsRound(Double(utf16Len(line)) / t.charsPerToken))
}
return tokens
}
// MARK: - planner API
/// Sum of per-section token estimates (framing tokens are the caller's
/// job). Mirrors `inputTokenTotal()` in the TS lib.
static func inputTokenTotal(_ sections: [PlanSection]) -> Int {
sections.reduce(0) { $0 + estimateTokens($1.text) }
}
/// Plan one section set against one model's context window. Returns
/// `nil` for an unknown model id (window math is `fitsWindow`'s, never
/// re-derived). Mirrors `planWindow()` in the TS lib.
static func planWindow(
_ sections: [PlanSection], _ modelId: String,
outputReserve: Int = 0, models: [Model] = []
) -> WindowPlan? {
let inputTokens = inputTokenTotal(sections)
// Fit check inlined from fitsWindow() in src/lib/ai/models.ts.
guard let m = models.first(where: { $0.id == modelId }) else { return nil }
let free = m.contextWindow - inputTokens
return 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 `planAll()` in the TS lib.
static func planAll(
_ sections: [PlanSection], _ modelIds: [String],
outputReserve: Int = 0, models: [Model] = []
) -> [WindowPlan] {
modelIds.compactMap {
planWindow(sections, $0, outputReserve: outputReserve, models: models)
}
}
}
/// Minimal ASCII-whitespace trim (the stdlib's `trimmingCharacters(in:)`
/// lives in Foundation; this port is stdlib-only). On StringProtocol so both
/// `String` and `Substring` get it.
private extension StringProtocol {
var trimmingWhitespace: Substring {
var s = Substring(self)
while let f = s.first, f == " " || f == "\t" || f == "\n" || f == "\r" || f == "\u{0B}" || f == "\u{0C}" {
s = s.dropFirst()
}
while let l = s.last, l == " " || l == "\t" || l == "\n" || l == "\r" || l == "\u{0B}" || l == "\u{0C}" {
s = s.dropLast()
}
return s
}
}
// ---------- tests (showcase-only; the canonical suite lives in src/lib) ----------
// Build with -DCWP_TEST and call ContextWindowPlannerTests.run().
#if CWP_TEST
enum ContextWindowPlannerTests {
/// A 1600-char single line of 'a' is pure prose: 1600 / 4 = 400 tokens.
private static func twoSections() -> [ContextWindowPlanner.PlanSection] {
let lineA = String(repeating: "a", count: 1600)
return [.init(label: "sys", text: lineA), .init(label: "docs", text: lineA)]
}
static func run() {
let two = twoSections()
// input totals
assert(ContextWindowPlanner.inputTokenTotal(two) == 800)
assert(ContextWindowPlanner.inputTokenTotal([]) == 0)
assert(ContextWindowPlanner.inputTokenTotal([.init(label: "sys", text: "")]) == 0)
// plans two 400-token sections against beta-pro
let p = ContextWindowPlanner.planWindow(two, "beta-pro", models: ContextWindowPlanner.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
let q = ContextWindowPlanner.planWindow(two, "beta-pro", outputReserve: 1_000_000, models: ContextWindowPlanner.sampleModels)!
assert(q.fits)
assert(!q.outputReserveOk)
// reserve exactly equal to free is ok
let r = ContextWindowPlanner.planWindow(two, "beta-pro", outputReserve: 999_200, models: ContextWindowPlanner.sampleModels)!
assert(r.outputReserveOk)
// smaller window leaves 199,200 free
let s = ContextWindowPlanner.planWindow(two, "alpha-mini", models: ContextWindowPlanner.sampleModels)!
assert(s.contextWindow == 200_000)
assert(s.free == 199_200)
assert(s.fits)
// unknown model id returns nil
assert(ContextWindowPlanner.planWindow(two, "ghost", models: ContextWindowPlanner.sampleModels) == nil)
// no sections: full window free
let t = ContextWindowPlanner.planWindow([], "beta-pro", models: ContextWindowPlanner.sampleModels)!
assert(t.inputTokens == 0)
assert(t.free == 1_000_000)
assert(t.fits)
// overflow: fits false, reserve false
let big = [ContextWindowPlanner.sec("big", String(repeating: "z", count: 4_400_000))]
let u = ContextWindowPlanner.planWindow(big, "beta-pro", models: ContextWindowPlanner.sampleModels)!
assert(u.inputTokens == 1_100_000)
assert(u.free == -100_000)
assert(!u.fits)
assert(!u.outputReserveOk)
// plan_all drops unknown ids and keeps order
let plans = ContextWindowPlanner.planAll(two, ["beta-pro", "alpha-mini", "ghost"], models: ContextWindowPlanner.sampleModels)
assert(plans.count == 2)
assert(plans[0].id == "beta-pro")
assert(plans[1].id == "alpha-mini")
assert(plans[1].free == 199_200)
assert(ContextWindowPlanner.planAll(two, [], models: ContextWindowPlanner.sampleModels).isEmpty)
print("context-window-planner (Swift): all tests passed")
}
}
#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 →