Package Exports
- loon-core
- loon-core/package.json
Readme
LOON — LLM-Optimized Object Notation
Token-efficient serialization for data pipelines and LLM prompts.
LOON converts structured data (JSON / CSV / XML / YAML / trees) into a compact wire format that minimizes tokens while staying losslessly reversible. Where most formats pick one trade-off, LOON ships three modes — one tuned for machine-to-machine transmission, one for direct LLM consumption, one for small or irregular objects — so you never pay for compression a given job doesn't need.
Think of it as a translation layer: keep JSON in your code, encode to LOON at the boundary where tokens cost money.
[!TIP] On a real 1,000-row retrieval session (Gemini 3 Flash), LOON
llmused 64% fewer tokens than JSON and 47% fewer than TOON, at 60% lower cost than JSON. See Benchmarks.
Table of Contents
- Why LOON?
- Key Features
- The Three Modes
- When Not to Use LOON
- Benchmarks
- Quick Start
- CLI
- Wire Format
getSpec()— whenfullmust talk to an LLM- Sessions & Context Caching
- API
- Architecture
- License
Why LOON?
LLM context windows keep growing, but tokens still cost money — and JSON is verbose. Every brace, quote, and repeated key is billed on every request. A uniform array of records pays for its field names once per row:
[
{ "id": 1, "name": "Alice", "dept": "Engineering", "salary": 120000, "active": true },
{ "id": 2, "name": "Bob", "dept": "Sales", "salary": 98000, "active": false }
]LOON llm mode declares the schema once and streams plain rows — no
braces, no repeated keys, no quoting of unambiguous values, while staying
readable to a model with zero primer:
@T1[2]{id,name,dept,salary,active}
1,Alice,Engineering,120000,true
2,Bob,Sales,98000,falseFor storage or system-to-system transfer, full mode goes further (Base36
integers, sequences, dictionaries) and is restored bit-for-bit by the decoder.
Key Features
- Token-Efficient & Verified: On a real billed retrieval session, LOON
llmcut total tokens 64% vs JSON and 47% vs TOON at 100% accuracy (see Benchmarks). - Three Modes, One Job Each:
fullfor transmission/storage,llmfor direct model reading,compactfor small/irregular data — no compromise format. - Lossless Round-Trips: A deterministic decoder restores the original JSON exactly; all modes recover the benchmark datasets 10/10.
- No Primer Needed (
llm): Plain decimal numbers, literaltrue/false/null, schema declared once. The model reads it; it never has to decode it. - Multi-Format Bridges: Encode from and decode to JSON, CSV, XML, YAML, and nested trees through one API.
- Prompt-Cache Friendly:
LoonSessionsplits a cacheable schema/spec prefix from per-call rows, so repeated queries bill mostly cache reads. - CLI Included:
npx loon-core data.json— convert, round-trip, and see token savings with--stats.
The Three Modes
| Mode | What it's for | Reads like |
|---|---|---|
full |
Bulk transmission / .loon storage. System → system, decoder → decoder. Maximum compression; LLM-readability is not a design constraint. |
Dense protocol output |
llm |
Direct LLM consumption. A model — cloud or local, reasoning or not — reads the payload with no spec or primer. | Labeled CSV |
compact |
Small / non-uniform datasets. Single deep objects, sparse rows, anything where a column schema can't amortize itself. | key: value blocks |
Plus:
compat— JSON-hybrid ({S,T,R}arrays). A 100%-valid-JSON escape hatch for environments that must parse withJSON.parse.local— deprecated alias ofllm(kept for back-compat).
Design rule.
fullis allowed every compression trick because its reader is a decoder.llmrejects every trick that would force the model to compute (Base36, sequences, dictionaries, suffix reattachment) and keeps only the tricks that de-duplicate structure — schema declared once, object arrays tabulated, nested objects grouped. The dividing line is: does this require execution to read?
When Not to Use LOON
- Deeply nested, non-uniform objects: with no uniform arrays to tabulate,
compactmode helps but the win shrinks — compact JSON can be competitive. - Pure flat tables consumed by code, not an LLM: CSV is marginally smaller. LOON adds structure (schema, length, type guards) that pays off for model reliability, not for a CSV parser.
- You need raw
JSON.parseon the wire: usecompatmode (valid JSON) and accept a smaller token saving. - Latency-critical local models: measure first. Token count is not always the bottleneck for quantized/local inference.
Benchmarks
Real billed session — LOON vs TOON vs JSON (Gemini 3 Flash)
Same dataset (fakestore: 1,000 products × id, price, category, rating_rate, rating_count), same ~10 data-retrieval questions, same model
(gemini-3-flash-preview), prompt caching on. Each format was fed to the
model and billed by the provider. Raw exports:
token-usage-31843f03.json (LOON),
token-usage-4eedd7fe.json (TOON),
token-usage-5ed44cd3.json (JSON).
1,000 products × ~10 retrieval questions · Gemini 3 Flash · prompt-cached
(total billed tokens — lower is better)
LOON llm ███████░░░░░░░░░░░░░ 272,048 tokens │ $0.047429 ← best
TOON █████████████░░░░░░░ 509,094 tokens │ $0.081574 (+87% tokens)
JSON ████████████████████ 761,458 tokens │ $0.118791 (+180% tokens)| Format | Input | Output | Total | Cost | Calls | Avg $/call |
|---|---|---|---|---|---|---|
LOON llm |
257,334 | 14,714 | 272,048 | $0.047429 | 11 | $0.00431 |
| TOON | 497,516 | 11,578 | 509,094 | $0.081574 | 10 | $0.00816 |
| JSON (pretty) | 751,298 | 10,160 | 761,458 | $0.118791 | 10 | $0.01188 |
[!TIP] LOON
llmvs JSON: −64.3% total tokens, −60.1% cost. LOONllmvs TOON: −46.6% total tokens, −41.9% cost. Normalized per call (LOON ran one extra question): $0.0043/call vs TOON $0.0082 and JSON $0.0119 — LOON is ~47% cheaper per call than TOON.
Honest read of the numbers. LOON llm carries the highest output and
reasoning tokens of the three (14.7k out, 10.8k reasoning) and ran one extra
call — yet still lands lowest on total tokens and cost, because its input is
dramatically smaller. The whole game here is input: a 1,000-row payload is
re-sent each turn, so a compact schema-once layout compounds across the session.
Token & cache breakdown per format
| Format | Input no-cache | Input cache-read | Output text | Output reasoning |
|---|---|---|---|---|
LOON llm |
85,791 | 171,543 | 3,918 | 10,796 |
| TOON | 72,213 | 425,303 | 3,026 | 8,552 |
| JSON | 241,080 | 510,218 | 2,566 | 7,594 |
Cache writes were 0 for all three (provider auto-cache). JSON's raw payload is so large that even with heavy cache reads its no-cache portion alone (241k) exceeds LOON's entire input.
Payload tokens (vs json-compact, GPT-4o)
Static encoding size across structural patterns:
| Dataset | TOON | LOON llm |
LOON full |
|---|---|---|---|
| Uniform employees | −36.8% | −41.9% | −51.5% |
| E-commerce (nested) | +5.4% | −35.6% | −42.9% |
| Time-series | −35.8% | −41.0% | −57.8% |
| Event logs | +19.9% | −30.4% | −32.7% |
full is the storage / transmission winner everywhere. llm beats TOON on
every dataset and remains LLM-readable without a spec.
full needs a spec; llm doesn't
Single-question retrieval, same payload, billed tokens (Gemini 3 Flash):
| Format | Accuracy | Input | Output | Total billed |
|---|---|---|---|---|
LOON llm |
100% | 3,267 | 332 | 3,599 |
| TOON | 100% | 3,365 | 269 | 3,633 |
| json-compact | 100% | 4,746 | 185 | 4,931 |
LOON full |
80% (no spec) | 2,662 | 3,398 | 6,060 |
full minimizes input by deferring work to the model — output cost then
blows up because it must mentally decode every Base36 / sequence / dictionary
cell. Fix it with getSpec(), or
simply use llm mode when the consumer is an LLM.
Round-trip fidelity
All four LOON modes recover all ten benchmark datasets losslessly (10 / 10).
Full reports in ../comprehension-benchmark/benchmarks/results/.
Quick Start
npm install loon-coreimport { Loon } from 'loon-core';
const loon = new Loon();
const data = [
{ id: 1, name: 'Alice', dept: 'Engineering', salary: 120000, active: true },
{ id: 2, name: 'Bob', dept: 'Sales', salary: 98000, active: false },
];
// LLM consumption (default for prompts)
loon.toLOON(data, { mode: 'llm' });
// Bulk transmission / storage (not for raw LLM reading)
loon.toLOON(data, { mode: 'full' });
// Small / irregular data
loon.toLOON(data, { mode: 'compact' });
// Decode (auto-detects the mode)
loon.fromLOON(encoded);If mode is omitted it is auto-selected: compact for empty or non-uniform
input, micro for 1–4 rows, full for ≥ 5 uniform rows. Override with
{ mode: 'llm' } when the target is a model.
CLI
No installation required — use it instantly with npx:
npx loon-core data.jsonOr install globally and use the loon command:
npm install -g loon-coreOptions
| Flag | Alias | Description |
|---|---|---|
--from <fmt> |
-f |
Input format: json csv xml yaml loon (auto-detect) |
--to <fmt> |
-t |
Output format: loon json csv xml yaml (default: loon) |
--mode <mode> |
-m |
Encoding mode: full llm compact (default: auto) |
--output <file> |
-o |
Write output to file instead of stdout |
--indent <n> |
-i |
JSON output indentation (default: 2) |
--stats |
-s |
Show token estimate and savings after encoding |
--verbose |
-v |
Print full stack traces on errors |
--help |
-h |
Show help |
Token statistics
Pass --stats to see a token estimate before and after encoding. Uses a
chars/4 heuristic — fast, no API key required.
✔ data.json → output.loon
ℹ Token estimate: ~15,145 (json) → ~8,745 (loon)
✔ Saved ~6,400 tokens (-42%) [strong]Output is colour-coded in TTY terminals and plain text when piped.
Examples
# JSON → LOON (auto mode)
loon data.json
# JSON → LOON llm mode + token stats
loon data.json -m llm --stats
# JSON → LOON full compression, write to file
loon data.json -m full -o output.loon
# CSV → LOON
loon data.csv -f csv -m llm
# LOON → JSON
loon data.loon -t json
# Pipe from stdin
echo '[{"id":1,"name":"Ada","role":"dev"}]' | loon
# Round-trip: JSON → LOON → JSON
cat data.json | loon | loon -f loon -t jsonWire Format
full mode — maximum compression
The format that lives in a .loon file or rides between two services.
S:@T1[N]=[col:type,...] schema: row count + columns with type codes
A:fullName,... column aliases (only if names were abbreviated)
DC:col,... integer columns stored decimal, not Base36
C:col=value constant column (omitted from every row)
Q:col=start,step integer arithmetic sequence
QF:col=start,step float arithmetic sequence
QS:col=start,step,prefix string sequence (prefix + counter)
FP:d=col,... fixed-point: row token ÷ 10^d = value
X:col=suffix common suffix stripped, re-appended on decode
D:col={tok:val,...} semantic dictionary (token → value)
D:__global__={tok:prefix} shared prefix dictionary (backs `$tok` cells)
D:defaults=col=val,... per-column default; `~` in a row means "use it"
DL:col=firstValue delta encoding (row tokens are signed deltas)
NM:col=mean,std,sigmaT,mT z-score normalization (LOSSY)
LY:NM marks payload contains lossy NORM columns
AS:col=k1,k2,... uniform object-array sub-schema (see below)
@T1: start of the data block
F:csv rows are comma-delimited
<data rows>
fullis not meant to be read by an LLM directly. If you must, prependgetSpec(encoded)to the prompt — and even then expect output-token costs to be higher thanllmmode, because the model has to mentally decode every Base36 / sequence / dictionary cell.
llm mode — self-evident, LLM-readable
One header line. Plain decimal rows. JSON-style literals.
@T1[N]{id,name,email,dept,salary,active}
C:status=active (optional, when a constant column exists)
AS:items=sku,name,qty (optional, for object-array columns)
1,Alice,alice@x.com,Engineering,120000,true
2,Bob,bob@x.com,Sales,98000,false
3,Carol,carol@x.com,Marketing,null,trueRules the model can apply at sight:
- Numbers bare:
120000. Decoded asNumber(token). - Booleans literal:
true/false. - Null literal:
null(same single BPE token JSON uses). - Strings bare when unambiguous:
Alice. - Strings that look like numbers / bools / null are quote-wrapped:
"123","true","null". The decoder strips the quotes and keeps the string as a string. - Absent key in this row (sparse / non-uniform):
+.
No :type codes in the header — the model infers types from cell shape. That
alone saves ~2 tokens per column. No DC: / @T1: / F:csv scaffolding
either; the @…{…} line is its own start marker.
compact mode — small or irregular data
id: 1
name: Alice
tags[3]: a,b,c scalar array, length 3
items[2]{sku,qty}: A,1;B,2 uniform object array
---
id: 2
...Single deep objects (configs) use an indented hierarchy instead of repeated dot-notation keys.
Row tokens (shared)
| Token | Meaning |
|---|---|
^ |
null (full mode) — llm mode writes the literal null |
~ |
use the column default (full only) |
+ |
key absent from this row — omit it (sparse / non-uniform schema) |
!value |
raw literal string (bypass the dictionary; full only) |
. |
a row that is entirely defaults |
*N[...] |
run-length: repeat the bracketed row N times (full only) |
AS: — uniform object-array sub-schema
A column holding an array of same-shape objects (items: [{sku,name,qty}, …])
would otherwise be inline JSON with the keys repeated on every element. AS:
declares the shared shape once; each cell carries values only:
AS:items=sku,name,qty
cell: A|Mouse|1;B|Cable|2 → [{sku:A,name:Mouse,qty:1},{sku:B,name:Cable,qty:2}]Fields within an object are |-separated; objects are ;-separated.
Type codes (full mode)
i integer · f float · s string · b boolean · a array · o object
llm mode omits the codes — types are inferred from cell shape.
getSpec() — when full must talk to an LLM
full is built for parsers. If you need an LLM to read a full payload (for
example: you stored data in .loon, you now want a model to query it),
getSpec(encoded) returns a minimal decode spec (200–600 tokens) covering only
the headers that this specific payload actually uses, plus a worked walkthrough
of row 0.
import { getSpec } from 'loon-core';
const encoded = loon.toLOON(data, { mode: 'full' });
const spec = getSpec(encoded); // { text, sections, estimatedTokens }
// prepend spec.text to the prompt; the model can now decode `full`.llm mode does not need a spec — that is the whole point of it.
Sessions & Context Caching
For repeated calls against the same data shape, LoonSession separates the
cacheable prompt prefix from the per-call data block. LLM providers cache an
identical prefix and bill it at a fraction of the normal rate; put the spec and
the schema there and the per-call cost shrinks to just the rows.
import { LoonSession } from 'loon-core';
const s = new LoonSession();
s.init(firstBatch, { mode: 'full' });
// System prompt (mark it for prompt caching): s.primer ← getSpec() + schema
// User message 1: s.dataBlock
for (const batch of moreBatches) {
send(s.encodeRows(batch).dataBlock); // only the rows
}splitLoon(encoded) exposes the raw { schema, dataBlock } split for custom
integrations.
Prompt caching reduces input cost. It does not touch output tokens. A model that has to reason hard about a format still pays full price on output. That is why
llmmode beatsfull + cached specfor direct LLM consumption:llm's output token cost is small because the format requires no reasoning to read.
API
| Method | Behavior |
|---|---|
toLOON(data, opts?) / encode |
JSON array → LOON |
fromLOON(loon) / decode |
LOON → JSON array |
fromCSV / fromXML / fromYAML |
other formats → LOON |
toCSV / toXML / toYAML |
LOON → other formats |
fromTree(tree, opts?) |
tree (nested objects) → LOON (TREE: header) |
toTree(loon) |
LOON → tree |
chunk(data, opts) |
split into context-window-sized LOON chunks |
encodeStream / fromLOONStream |
async streaming codec |
getSpec(loon) |
minimal decode spec for a payload |
LoonSession |
multi-call session: primer, dataBlock, encodeRows, decode |
splitLoon(loon) |
{ schema, dataBlock, full } |
validateDecode(loon, rows) |
post-decode structural check |
repairHint(loon, errors) |
minimal retry prompt for an LLM that mis-decoded |
reset() |
clear per-instance schema state |
Options (LoonOptions)
| Option | Effect |
|---|---|
mode |
force full / llm / compact / compat |
fields |
column projection |
maxDecimals |
trim float precision before encoding |
tableId |
override the default schema id (T1) |
outFile |
write encoded output to a file (Node) |
checkpointEvery |
emit a #CKP: schema checkpoint every N rows (full) |
primaryCols |
promote columns to the front + force decimal |
norm |
z-score normalize float columns (lossy; full only) |
Architecture
Input (JSON / CSV / XML / YAML / tree)
│
Mode Selector ──────────────┐
│ │ (auto-pick when mode omitted)
┌────┴─────┬──────────┐ │
▼ ▼ ▼ ▼
full llm compact compat
│ │ │ │
└── Adaptive ─┘ │ │
pipeline │ │
│ │ │
└─────────────────┴─────┘
│
LOON output| Module | Path | Role |
|---|---|---|
| Public API | src/index.ts |
Loon facade, mode routing |
| Adaptive encoder | src/encoder/adaptive/ |
analyzer → header → rows |
| Adaptive decoder | src/decoder/adaptive/ |
header-parser → row-reconstructor |
| Compact codec | src/encoder/compact.ts, src/decoder/compact.ts |
key: value + indent |
| Adaptive engine | src/compression/adaptive.ts |
cell compress/decompress, dictionaries, AS: tabular |
| Mode selector | src/compression/mode-selector.ts |
dataset-shape heuristics |
| State manager | src/state/state-manager.ts |
per-schema context + reverse-dict cache |
| Spec generator | src/utils/get-spec.ts |
getSpec() minimal decode spec |
| Session | src/session.ts |
LoonSession, splitLoon |
| Codecs | src/codecs/ |
CSV / XML / YAML / tree bridges |
full and llm share one analysis pipeline. The analyzer gates off every
compute-requiring primitive (Base36, sequences, dictionaries, defaults,
suffixes, fixed-point, delta, NORM, RLE, anchor rows) when the mode is llm.
What survives is structural-only: the schema, C: constants, and AS:
sub-schemas. The header emitter also drops :type codes and replaces ^ (null
sentinel) with the literal null for llm mode.
License
MIT © LOON Thesis Team