Skip to content

Context Window Planner — Zig 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 Zig 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: Zig (Zig 0.13, 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 panics (public API returns plain values,
//!     no allocator required).
//!   - Functionally equivalent to the TS reference: same inputs -> same outputs.
//!   - Self-contained: std only (std.json exists but pulls the full reader
//!     machinery; this port 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 Zig's std 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). Zig
//!     slices are UTF-8 bytes, so line arithmetic goes through `utf16Len`,
//!     which decodes code points and counts the same unit.
//!   - Rounding: `jsRound` is `@floor(x + 0.5)` — JS `Math.round` rounds
//!     halfway cases up; `@round` rounds away from zero (identical on the
//!     non-negative numbers used here, but the helper pins the formula).

const std = @import("std");

/// One labeled block of the prompt (system / docs / history / ...).
/// Mirrors the TS `PlanSection` interface.
pub const PlanSection = struct {
    /// Section label, e.g. "system" or "docs".
    label: []const u8,
    /// The section's raw text.
    text: []const u8,
};

/// Convenience constructor mirroring the TS object literal `{ label, text }`.
pub fn sec(label: []const u8, text: []const u8) PlanSection {
    return .{ .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.
pub const Model = struct {
    /// Model id, e.g. "beta-pro".
    id: []const u8,
    /// Total context window in tokens.
    context_window: i64,
    /// The model's output cap (informational).
    max_output: i64,
};

/// Sample table for standalone use (mirrors the shared test fixtures).
/// Production code passes the model snapshot instead.
pub const sample_models = [_]Model{
    .{ .id = "alpha-mini", .context_window = 200_000, .max_output = 10_000 },
    .{ .id = "beta-pro", .context_window = 1_000_000, .max_output = 10_000 },
    .{ .id = "gamma-open", .context_window = 100_000, .max_output = 10_000 },
};

/// Result of `planWindow`. Field-for-field twin of the TS `WindowPlan`
/// interface.
pub const WindowPlan = struct {
    /// The model id planned against.
    id: []const u8,
    /// Sum of per-section token estimates.
    input_tokens: i64,
    /// The model's context window.
    context_window: i64,
    /// Context tokens left after the request; negative on overflow.
    free: i64,
    /// Raw fit: free >= 0.
    fits: bool,
    /// Room for the output reserve: free >= reserve.
    output_reserve_ok: bool,
    /// The model's output cap (informational).
    max_output: i64,
};

/// 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).
const ContentType = enum {
    prose,
    code,
    json,
    cjk,

    fn charsPerToken(self: ContentType) f64 {
        return switch (self) {
            .prose => 4.0,
            .code => 3.5,
            .json => 3.0,
            .cjk => 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. Input must
/// be valid UTF-8 (page text always is).
fn utf16Len(s: []const u8) i64 {
    var units: i64 = 0;
    var it = std.unicode.Utf8View.initUnchecked(s).iterator();
    while (it.nextCodepoint()) |cp| {
        units += if (cp > 0xFFFF) 2 else 1;
    }
    return units;
}

/// 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.
fn hasCjk(s: []const u8) bool {
    var it = std.unicode.Utf8View.initUnchecked(s).iterator();
    while (it.nextCodepoint()) |cp| {
        if ((cp >= 0x4E00 and cp <= 0x9FFF) or
            (cp >= 0x3040 and cp <= 0x30FF) or
            (cp >= 0xAC00 and cp <= 0xD7AF))
        {
            return true;
        }
    }
    return false;
}

/// Reports whether `c` is one of the code-flavored symbols counted by
/// `CODE_SYMBOL_RE` (`{}();=<>[]#`).
fn isCodeSymbol(c: u8) bool {
    return c == '{' or c == '}' or c == '(' or c == ')' or c == ';' or
        c == '=' or c == '<' or c == '>' or c == '[' or c == ']' or c == '#';
}

const ws = " \t\n\r\x0b\x0c";

/// JS `Math.round`: halfway cases round up (`@floor(x + 0.5)`).
fn jsRound(x: f64) i64 {
    return @intFromFloat(@floor(x + 0.5));
}

/// Classifies a single line by its shape. Order: json, cjk, code, prose.
/// Inlined from `detectLineType()` in src/lib/tokenEstimator.ts.
fn detectLineType(line: []const u8) ContentType {
    const trimmed = std.mem.trim(u8, line, ws);
    // JSON-ish: opens like a JSON fragment AND carries a separator.
    const starts_jsonish = trimmed.len > 0 and
        (trimmed[0] == '{' or trimmed[0] == '}' or
        trimmed[0] == '[' or trimmed[0] == '"');
    if (starts_jsonish and
        (std.mem.indexOfScalar(u8, line, ':') != null or
        std.mem.indexOfScalar(u8, line, ',') != null))
    {
        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.
    const length = utf16Len(line);
    var symbols: i64 = 0;
    for (line) |c| {
        if (isCodeSymbol(c)) symbols += 1;
    }
    const density = if (length > 0)
        @as(f64, @floatFromInt(symbols)) / @as(f64, @floatFromInt(length))
    else
        0.0;
    const ends_code = std.mem.endsWith(u8, trimmed, ";") or
        std.mem.endsWith(u8, trimmed, "{") or std.mem.endsWith(u8, trimmed, "}");
    if (density > 0.08 or ends_code) {
        return .code;
    }
    return .prose;
}

/// A strict JSON syntax validator — the exact grammar `JSON.parse` accepts,
/// walked with a byte cursor. (Multibyte UTF-8 inside strings never contains
/// an ASCII byte, so byte-level scanning is safe.)
const JsonParser = struct {
    bytes: []const u8,
    pos: usize,

    fn init(text: []const u8) JsonParser {
        return .{ .bytes = text, .pos = 0 };
    }

    fn skipWs(self: *JsonParser) void {
        while (self.pos < self.bytes.len) {
            const b = self.bytes[self.pos];
            if (b == ' ' or b == '\t' or b == '\n' or b == '\r') {
                self.pos += 1;
            } else {
                break;
            }
        }
    }

    fn peek(self: *const JsonParser) ?u8 {
        return if (self.pos < self.bytes.len) self.bytes[self.pos] else null;
    }

    fn eat(self: *JsonParser, b: u8) bool {
        if (self.peek() == b and self.pos < self.bytes.len) {
            self.pos += 1;
            return true;
        }
        return false;
    }

    fn literal(self: *JsonParser, lit: []const u8) bool {
        if (std.mem.startsWith(u8, self.bytes[self.pos..], lit)) {
            self.pos += lit.len;
            return true;
        }
        return false;
    }

    /// value := ws* (object | array | string | number | 'true' | 'false' | 'null') ws*
    fn value(self: *JsonParser) bool {
        self.skipWs();
        const b = self.peek() orelse return false;
        return switch (b) {
            '{' => self.object(),
            '[' => self.array(),
            '"' => self.string(),
            '-', '0'...'9' => self.number(),
            't' => self.literal("true"),
            'f' => self.literal("false"),
            'n' => self.literal("null"),
            else => false,
        };
    }

    /// object := '{' ws* (string ws* ':' value (ws* ',' ...)*)? ws* '}'
    fn object(self: *JsonParser) bool {
        if (!self.eat('{')) return false;
        self.skipWs();
        if (self.eat('}')) return true;
        while (true) {
            if (!self.string()) return false;
            self.skipWs();
            if (!self.eat(':')) return false;
            if (!self.value()) return false;
            self.skipWs();
            if (self.eat(',')) {
                self.skipWs();
            } else {
                return self.eat('}');
            }
        }
    }

    /// array := '[' ws* (value (ws* ',' ws* value)*)? ws* ']'
    fn array(self: *JsonParser) bool {
        if (!self.eat('[')) return false;
        self.skipWs();
        if (self.eat(']')) return true;
        while (true) {
            if (!self.value()) return false;
            self.skipWs();
            if (self.eat(',')) {
                self.skipWs();
            } else {
                return self.eat(']');
            }
        }
    }

    /// string := '"' (escape | any byte >= 0x20)* '"'
    /// escape := '\' ('"' | '/' | '\' | 'b' | 'f' | 'n' | 'r' | 't' | 'u' hex4)
    fn string(self: *JsonParser) bool {
        if (!self.eat('"')) return false;
        while (self.pos < self.bytes.len) {
            const b = self.bytes[self.pos];
            if (b == '"') {
                self.pos += 1;
                return true;
            }
            if (b == '\\') {
                self.pos += 1;
                if (self.pos >= self.bytes.len) return false;
                const esc = self.bytes[self.pos];
                self.pos += 1;
                switch (esc) {
                    '"', '/', '\\', 'b', 'f', 'n', 'r', 't' => {},
                    'u' => {
                        var i: usize = 0;
                        while (i < 4) : (i += 1) {
                            const h = self.peek() orelse return false;
                            const hex = (h >= '0' and h <= '9') or
                                (h >= 'a' and h <= 'f') or (h >= 'A' and h <= 'F');
                            if (!hex) return false;
                            self.pos += 1;
                        }
                    },
                    else => return false,
                }
            } else if (b < 0x20) {
                // Raw control characters are not allowed inside strings.
                return false;
            } else {
                self.pos += 1;
            }
        }
        return false; // unterminated string
    }

    /// number := '-'? int frac? exp? — no leading zeros, like JSON.parse.
    fn number(self: *JsonParser) bool {
        _ = self.eat('-');
        const first = self.peek() orelse return false;
        switch (first) {
            '0' => self.pos += 1,
            '1'...'9' => {
                while (self.peek()) |p| {
                    if (p < '0' or p > '9') break;
                    self.pos += 1;
                }
            },
            else => return false,
        }
        if (self.peek() == @as(?u8, '.')) {
            self.pos += 1;
            var digits: usize = 0;
            while (self.peek()) |p| {
                if (p < '0' or p > '9') break;
                self.pos += 1;
                digits += 1;
            }
            if (digits == 0) return false;
        }
        if (self.peek() == @as(?u8, 'e') or self.peek() == @as(?u8, 'E')) {
            self.pos += 1;
            if (self.peek() == @as(?u8, '+') or self.peek() == @as(?u8, '-')) {
                self.pos += 1;
            }
            var digits: usize = 0;
            while (self.peek()) |p| {
                if (p < '0' or p > '9') break;
                self.pos += 1;
                digits += 1;
            }
            if (digits == 0) 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.
fn isValidJson(text: []const u8) bool {
    if (std.mem.trim(u8, text, ws).len == 0) return false;
    var p = JsonParser.init(text);
    if (!p.value()) return false;
    p.skipWs();
    return p.pos == p.bytes.len; // reject trailing garbage
}

/// 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.
fn estimateTokens(text: []const u8) i64 {
    // AUTO + whole-text JSON: json's 3 chars/token rate applies to every
    // line, not just the reported content type.
    const whole_text_json = isValidJson(text);
    var tokens: i64 = 0;
    // Split on LF or CRLF (text.split(/\r?\n/)): strip the optional CR that
    // belongs to the newline, then split on LF. A lone CR is NOT a break.
    var rest = text;
    while (rest.len > 0) {
        const nl = std.mem.indexOfScalar(u8, rest, '\n');
        var line = if (nl) |i| rest[0..i] else rest;
        if (line.len > 0 and line[line.len - 1] == '\r') {
            line = line[0 .. line.len - 1];
        }
        if (std.mem.trim(u8, line, ws).len > 0) {
            const t: ContentType = if (whole_text_json) .json else detectLineType(line);
            const est = jsRound(
                @as(f64, @floatFromInt(utf16Len(line))) / t.charsPerToken(),
            );
            tokens += if (est >= 1) est else 1;
        }
        if (nl) |i| {
            rest = rest[i + 1 ..];
        } else {
            break;
        }
    }
    return tokens;
}

/// Sum of per-section token estimates (framing tokens are the caller's job).
/// Mirrors `inputTokenTotal()` in the TS lib.
pub fn inputTokenTotal(sections: []const PlanSection) i64 {
    var total: i64 = 0;
    for (sections) |s| total += estimateTokens(s.text);
    return total;
}

/// Plan one section set against one model's context window. Returns `null`
/// for an unknown model id (window math is `fitsWindow`'s, never
/// re-derived). Mirrors `planWindow()` in the TS lib.
pub fn planWindow(
    sections: []const PlanSection,
    model_id: []const u8,
    output_reserve: i64,
    models: []const Model,
) ?WindowPlan {
    const input_tokens = inputTokenTotal(sections);
    // Fit check inlined from fitsWindow() in src/lib/ai/models.ts.
    var found: ?Model = null;
    for (models) |m| {
        if (std.mem.eql(u8, m.id, model_id)) {
            found = m;
            break;
        }
    }
    const m = found orelse return null;
    const free = m.context_window - input_tokens;
    return .{
        .id = model_id,
        .input_tokens = input_tokens,
        .context_window = m.context_window,
        .free = free,
        .fits = free >= 0,
        .output_reserve_ok = free >= output_reserve,
        .max_output = m.max_output,
    };
}

/// Plan against several models; unknown ids are dropped from the result.
/// Writes at most `out.len` plans and returns the count written.
/// Mirrors `planAll()` in the TS lib.
pub fn planAll(
    sections: []const PlanSection,
    model_ids: []const []const u8,
    output_reserve: i64,
    models: []const Model,
    out: []WindowPlan,
) usize {
    var written: usize = 0;
    for (model_ids) |id| {
        if (written >= out.len) break;
        if (planWindow(sections, id, output_reserve, models)) |plan| {
            out[written] = plan;
            written += 1;
        }
    }
    return written;
}

// ---------- tests (showcase-only; the canonical suite lives in src/lib) ----------

/// A 1600-char single line of 'a' is pure prose: 1600 / 4 = 400 tokens.
fn twoSections(buf: []u8) [2]PlanSection {
    @memset(buf, 'a');
    return .{
        sec("sys", buf[0..1600]),
        sec("docs", buf[0..1600]),
    };
}

test "input totals" {
    var buf: [1600]u8 = undefined;
    const two = twoSections(&buf);
    try std.testing.expectEqual(@as(i64, 800), inputTokenTotal(&two));
    try std.testing.expectEqual(@as(i64, 0), inputTokenTotal(&[_]PlanSection{}));
    try std.testing.expectEqual(@as(i64, 0), inputTokenTotal(&[_]PlanSection{sec("sys", "")}));
}

test "plans two 400-token sections against beta-pro" {
    var buf: [1600]u8 = undefined;
    const two = twoSections(&buf);
    const p = planWindow(&two, "beta-pro", 0, &sample_models).?;
    try std.testing.expectEqualStrings("beta-pro", p.id);
    try std.testing.expectEqual(@as(i64, 800), p.input_tokens);
    try std.testing.expectEqual(@as(i64, 1_000_000), p.context_window);
    try std.testing.expectEqual(@as(i64, 999_200), p.free);
    try std.testing.expect(p.fits);
    try std.testing.expect(p.output_reserve_ok);
    try std.testing.expectEqual(@as(i64, 10_000), p.max_output);
}

test "reserve larger than free leaves raw fit true" {
    var buf: [1600]u8 = undefined;
    const two = twoSections(&buf);
    const p = planWindow(&two, "beta-pro", 1_000_000, &sample_models).?;
    try std.testing.expect(p.fits);
    try std.testing.expect(!p.output_reserve_ok);
}

test "reserve exactly equal to free is ok" {
    var buf: [1600]u8 = undefined;
    const two = twoSections(&buf);
    const p = planWindow(&two, "beta-pro", 999_200, &sample_models).?;
    try std.testing.expect(p.output_reserve_ok);
}

test "smaller window leaves 199,200 free" {
    var buf: [1600]u8 = undefined;
    const two = twoSections(&buf);
    const p = planWindow(&two, "alpha-mini", 0, &sample_models).?;
    try std.testing.expectEqual(@as(i64, 200_000), p.context_window);
    try std.testing.expectEqual(@as(i64, 199_200), p.free);
    try std.testing.expect(p.fits);
}

test "unknown model id returns null" {
    var buf: [1600]u8 = undefined;
    const two = twoSections(&buf);
    try std.testing.expect(planWindow(&two, "ghost", 0, &sample_models) == null);
}

test "no sections full window free" {
    const p = planWindow(&[_]PlanSection{}, "beta-pro", 0, &sample_models).?;
    try std.testing.expectEqual(@as(i64, 0), p.input_tokens);
    try std.testing.expectEqual(@as(i64, 1_000_000), p.free);
    try std.testing.expect(p.fits);
}

test "overflow fits false reserve false" {
    const big = [_]PlanSection{sec("big", "z" ** 4_400_000)};
    const p = planWindow(&big, "beta-pro", 0, &sample_models).?;
    try std.testing.expectEqual(@as(i64, 1_100_000), p.input_tokens);
    try std.testing.expectEqual(@as(i64, -100_000), p.free);
    try std.testing.expect(!p.fits);
    try std.testing.expect(!p.output_reserve_ok);
}

test "planAll drops unknown ids and keeps order" {
    var buf: [1600]u8 = undefined;
    const two = twoSections(&buf);
    const ids = [_][]const u8{ "beta-pro", "alpha-mini", "ghost" };
    var plans: [3]WindowPlan = undefined;
    const n = planAll(&two, &ids, 0, &sample_models, &plans);
    try std.testing.expectEqual(@as(usize, 2), n);
    try std.testing.expectEqualStrings("beta-pro", plans[0].id);
    try std.testing.expectEqualStrings("alpha-mini", plans[1].id);
    try std.testing.expectEqual(@as(i64, 199_200), plans[1].free);
    try std.testing.expectEqual(
        @as(usize, 0),
        planAll(&two, &[_][]const u8{}, 0, &sample_models, &plans),
    );
}

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 →