Skip to content

Releases: Davmunrey/Trazum

v2.3.0

Choose a tag to compare

@github-actions github-actions released this 31 Aug 11:06
2cc44e4

2.3.0 — The warning that cried wolf

Every profile run was warning that its prices were 68 days old, on prices
read that week.
The warning says, in these words, that the price table behind
every dollar here was last reviewed on that date. On a report of Claude and
OpenAI calls it named 2026-06-24 — a date belonging to two models that report
never touched — while the prices actually used had been read four days and zero
days earlier.

The date it printed is the oldest provider's, which is the right answer to
how old is this catalogue and the wrong answer to how old are the prices in
front of me
. Three surfaces used it: the CLI's profile, the MCP report and
the browser's bill.

The second cost is worse than the wrong figure. A warning that fires on
every run is one people stop reading, so on the day the table really is stale,
nothing has been said that was not said yesterday.

trazum models had already worked this out and prints its dates per provider,
because a reader pricing Claude calls should not be told two months when their
half was checked that morning. The fix reached one surface and not the three
that qualify a figure. It has now.

profile --json gains reportReviewed and reportAgeDays beside
lastReviewed and ageDays, which go on meaning exactly what they meant: the
table's own oldest provider. Two questions, two pairs of keys, and nothing a
consumer branches on changed underneath it.

The skill is written for any agent now, not for one

Trazum's agent-facing skill opened by telling the reader to run npm install
and then spelled every example as node packages/cli/dist/index.js, fifteen
times. An agent with an MCP client and no shell was handed a document about a
shell. An agent working in somebody else's repository was handed a path that
does not exist there.

Every command is spelled trazum <command> now, and one table at the top says
how to spell that for whichever door you have: a shell in a checkout, a shell
anywhere else, MCP tools and no shell, a library import, or a browser.

## Through MCP is new, and it is the point of the change. For an agent
with no shell the MCP server is the whole product, and the skill mentioned it
nowhere. It now carries the seven tools, the client-agnostic stdio wiring, and
the one rule that decides whether any of it works: the server never opens a
file, every tool takes the text, and an agent that passes a path gets nothing
back that means anything.

## Before you spend is new too, and it is the moment the skill has always
claimed to be for — a budget exists, a call is about to be made, nobody knows
whether it fits. Both doors are named, and so are the three ways to get it
wrong: cannot-tell is not a yes, the ceiling is never invented so a check can
pass, and what has been spent is measured while what the next call costs is an
estimate.

One claim in it was false. from-litellm, from-helicone and
from-langsmith were described as named as next but not built for the whole
arc after they shipped, so an agent asked about a LiteLLM export offered to
write a converter that had been there for releases. All five converters now sit
in one table, with from-otel named as the one to offer when you do not know
what somebody runs — it is the standards-based door and does not depend on
which vendor they chose.

Three guards now hold the skill to the code, all derived from it: every command
it tells an agent to run is one the CLI dispatches, its MCP tool table equals
the set the server registers in both directions, and every converter that
exists is mentioned. The false claim above is exactly what the third one
catches.

Prices

Nothing moved. OpenAI was read on 2026-08-31 and Anthropic, Google, DeepSeek
and Mistral within four days of it. xai and moonshot stay at 2026-06-24:
grok-4 is absent from the model list xAI's own docs serve and kimi-k2 from
the llms.txt Moonshot publishes, so there is no price to re-read, and moving
the date would say a check happened that did not. Neither is marked retired —
that needs the provider refusing a real request in its own words, and a page
that stopped mentioning a model has refused nothing.

v2.2.1

Choose a tag to compare

@github-actions github-actions released this 31 Aug 06:35
56955c0

2.2.1 — One price table read, two that no longer describe our models

OpenAI's prices were 68 days old. They have been read, and nothing had moved:
gpt-5 is still $1.25 in and $10 out per million tokens, gpt-5-mini $0.25 and
$2, gpt-5-nano $0.05 and $0.4. The date on that provider moves to 2026-08-31
because the table was read, which is the only thing that ever moves a date here.

Two traps on the way, both now written down beside the constant. The address had
changed — platform.openai.com/docs/pricing redirects to
developers.openai.com — and every note in this repository cited the old one, so
a reviewer who stops at the redirect reads nothing and moves the date anyway. And
the page publishes four tables for the same model: standard, batch, flex and
priority. Reading the wrong one halves or doubles every figure. The standard
table is identified by the other three being its multiples, not by trusting the
order they appear in — an order is a layout decision somebody can change on a
Tuesday.

The two that did not move, and why that is the honest answer

moonshot and xai stay at 2026-06-24. Their pages are readable now; that
blockage is gone. What is in the way instead is that the models this catalogue
prices are not on either page any more.
xAI publishes grok-4.3, 4.5, 4.6, 4.20
and grok-build; there is no grok-4. Moonshot publishes kimi-k3, k2.7-code,
k2.6, k2.5 and the V1 series; there is no kimi-k2.

So the $3/$15 carried for grok-4 and the $0.6/$2.5 carried for kimi-k2 cannot
be checked against anything, and a date that moved on that would certify a
reading that never happened.

Neither is marked retired either, and the restraint matters more than it looks.
This project only records a model as retired when the provider refuses a real
request for that id
, quoting the provider's own sentence. Absence from a
webpage is not a refusal, and writing one down without having received it would
be inventing the provider's words to close a ticket. That is the same failure as
inventing a price with a different hat on.

Both need an API key for the availability check to ask and be told no. Until
then the catalogue's own oldest-provider date goes on reading 2026-06-24 and
saying so on every report that quotes it — which is exactly what it is for.

v2.2.0

Choose a tag to compare

@github-actions github-actions released this 30 Aug 20:35
a9855d8

2.2.0 — Tested against inputs nobody wrote

Four thousand tests found nearly every defect this repository has ever fixed,
and they all share a blind spot: a fixture only contains what somebody thought
to put in it.
This release adds thirty properties that draw their inputs from
a seeded generator instead — thousands of prompts, logs and documents per run —
and holds the product to the promises its doctrine makes rather than to the
answers a hand-written example happens to produce.

What is now checked, and against what

The optimiser. That it never throws on any text at all; that two runs of the
same prompt agree; that a prompt never grows; that a disabled rule stays
disabled; that aggressive never saves less than safe; and that every
protected segment — a URL, an inline span of code, a fenced block, a
placeholder — comes back untouched. An optimiser that mangles an API endpoint to
save four tokens has broken the prompt it was asked to improve, and the saving
is the least interesting thing about that.

The rule this project repeats most often: a locale changes the report,
never the optimisation.
Every locale must produce the same optimised text, the
same token figures, the same rules fired and the same advisories raised. It is a
claim about every prompt in every language, which is exactly the kind of claim an
example can only illustrate. There is a second assertion beside it insisting the
prose does differ somewhere, because a locale suite that would still pass
against a translation file emptied to English is a suite that checks nothing
about locales at all.

The money. Slices add up to the total; the three groupings of a report agree
with each other; the order the lines arrived in changes nothing; adding a record
never makes a bill smaller; unpriced calls are never quietly given a price; and
every rate in the catalogue is a finite non-negative number on every date it can
be asked about. Repricing a receipt answers exactly what repricing the profile it
came from answers — including which traffic it refuses, because a refusal that
quietly stops happening reads as a saving.

That nothing travels. Credentials, absolute paths, a branch name and an
address are planted into every field a usage record can be made to carry —
including fields nobody declared, which is how content actually arrives — and
then searched for in the receipt, in all three shapes of the CSV export, and in
the profile the converters are built from. The promise is not that these are
redacted; it is that a usage record has nowhere to put them, so nothing
downstream can carry them. A model id and a label are excluded, because those
travel by design and the documentation says so out loud.

That a refusal is useful. trazum conform is the door a third party's
emitter knocks on, and the only thing it can hand back is a no. A no with
nothing after it is indistinguishable from a bug in the checker, and whoever
receives it starts guessing. Every refusal now has to arrive with the sentence
that makes it actionable, on every input at all.

The defect that found

requiredFieldsOf tells an emitter which fields its document must carry. It
left out schemaVersion — a field conform requires and refuses documents
for. The omission was deliberate and documented, and the reasoning was about
where the check lives rather than about the question being asked.

What makes it a real defect is that both callers in this repository had
already worked around it
, each adding the field back by hand. That is one
fact written in three places, which is exactly what conform.ts exists to
prevent one directory over. It is fixed, and the test that had been repairing
its subject's answer now reads the export whole.

That is a behaviour change to a published function, which is why this is a
minor version rather than a patch: anybody who built the same workaround now
gets schemaVersion twice.

Written rather than installed

The generators are a seeded pseudo-random number generator and a dozen shapes —
about two hundred readable lines. A property-testing library would have been a
development dependency and would not have broken the zero-runtime-dependency
promise on the front page, but in a repository that spends this much effort on
what it depends on, writing them is the cheaper side of the trade.

They are hostile on purpose: empty strings, a hundred kilobytes of one
character, code fences that never close, right-to-left overrides, NUL bytes,
lone surrogates, __proto__, NaN, Infinity, -0. A generator that only
produced tidy English would exercise the paths the example suites already
cover.

Every run is seeded and every failure prints its own reproduction command, so a
red CI log carries the exact incantation that reproduces it. The default seed
is fixed: a suite that fails on Tuesdays and passes on Wednesdays gets disabled
by whoever is on call, and then it guards nothing at all.

Three of these properties were wrong before the code was, and each says so
in its own file. The redaction suite took its "carried by design" set one record
at a time while folding a whole batch into one report, so a model id planted as
another record's session field was searched for in the very gap that must name
it. A property that was wrong first is the most useful comment a property can
carry.

v2.1.0

Choose a tag to compare

@github-actions github-actions released this 30 Aug 19:45
9aecd3e

2.1.0 — The receipt can be repriced

One question, now askable of a receipt: what would these calls have cost on
another model?

trazum has been able to answer that from a log since the repricing landed. A
receipt could not be asked it at all — and a receipt is the thing that leaves
your machine, so by the time anybody wants the answer the log has usually been
aged out by a retention policy, or is on a runner that no longer exists, or is
on somebody else's laptop. A comparison only the holder of the log can make is a
comparison only the person who least needs it can make.

The field that was missing, and why it was the one that mattered

Repricing refuses to price traffic the target could not have accepted. A cheaper
model with a smaller context window does not make a 400,000-token call cheaper —
it makes it impossible, and counting an impossible call's price difference as a
saving is exactly the flattering direction this product exists to refuse.

That refusal needs one number: the largest single call in a slice. A token
total cannot supply it, an average hides it, and a receipt did not carry it.

Meanwhile the two cache-write TTL fields on every receipt line have carried a
comment since 1.83.0 saying they are there "so a consumer can reprice this
traffic against another model's rates"
. The format was built for repricing
and stopped one field short of it.
It does not any more.

What is new

  • ReceiptLine.maxCallInputTokens — the largest single call in the slice, cache
    reads and writes included.
  • ReceiptLine.assumedWriteTtlCalls — calls whose cache-write TTL the log did
    not state, per line. The document already reported this organisation-wide as a
    gap; a comparison needs the resolution it travels at.
  • repriceReceipt(document, target, catalogue, on?) — the same question asked
    of a receipt.
  • RepriceableSlice and RepriceableInput, exported, because they are the
    shape both entry points feed.

Both new fields are counts. Neither can hold text, a path or an identifier, so
the redaction promise is unchanged and its guard is unchanged with it.

There is one implementation, and that was the constraint

repriceReceipt is not a second copy of the repricing. The loop that refuses
over-context traffic, excludes calls already billed on the target, keeps the two
write TTLs apart and states that the token counts are assumed to survive the
move — that loop is now reached through a narrow slice type, and both entry
points map onto it.

Two loops reading two shapes would drift on which traffic is refused, and a
refusal that quietly stops happening reads as a saving. So the guard is an
identity rather than a set of expected numbers: repricing a receipt answers
exactly what repricing the profile it came from answers — every figure,
every refused slice, every caveat. A planted receipt that lost its largest call
fails it immediately.

What this release does not change

No new command. The CLI still dispatches 46, and 2.0.0's promise was about
that surface: a command that exists keeps existing and keeps meaning what it
means. This adds two numbers to a published format and one function.

No prices were re-read. openai, moonshot and xai are still dated 2026-06-24
and still 67 days stale, and the product still says so on every figure derived
from them. The session that cut this release could not reach those three pages,
which is the same honest state 2.0.0 shipped in and is not improved by pretending
otherwise.

Nothing about a receipt's contents leaves that was not leaving before.

v2.0.0

Choose a tag to compare

@github-actions github-actions released this 30 Aug 17:02
3dfa984

2.0.0 — "Done"

No new analysis. No new command. This release exists to change what the README
describes from something growing into something finished, and the arc plan set
four conditions for saying that. Three held when they were audited. The fourth
did not, and finding out why is most of what this release contains.

The surface is frozen at 46 commands. That is the promise a major version
exists to make: the 46th was the receipt, and there will not be a 47th. What
follows 2.0 is maintenance — prices re-reviewed on their own per-provider
clocks, models added and retired, corrections, security.

The condition that failed

"Every analysis this product can perform offline, it performs."

ROADMAP.md was offering to build two things it had already shipped. The
editor extension went out as trazum-vscode in 1.86.0 and the per-model-family
tokenizer as @trazum/tokenizer-openai in 1.85.0, and the same file recorded
both as released two hundred lines above
while the section headed Under
consideration
went on calling them ideas — one of them with the words "still
unscheduled"
.

Both are gone from that section, and roadmap-truth.test.js holds it: no entry
may be titled after a workspace that ships, and every entry left must say what
blocks it. It derives from package.json's workspaces, so a package added later
arrives without being invited.

The three entries that remain are genuinely unbuilt and each names what stops
it: more contradiction axes (not enough real prompts to know which earn their
place), more locales (nobody who reads the language), and cost alerting (it
needs somewhere to run and something to remember).

Five analyses that read prompts too narrowly

Each of these found less than it claimed to, and each failed silently — the
worst kind, because a detector that reports nothing looks exactly like a prompt
with nothing wrong in it.

  • The schema reader named four formats in its own filter and could read one.
    It advertised JSON, YAML, TypeScript and more; only one path actually parsed.
  • The example detector was blind to nine of fourteen labellings, and six of
    those are the ones people actually write.
  • The tag Anthropic's own documentation tells people to use found nothing.
    <example> blocks were invisible to the few-shot analysis.
  • trazum prune stated something false with confidence on a prompt whose
    examples it could not separate.
  • The contradiction detector missed eight of eleven ordinary phrasings. The
    four axes were right; the fragments that recognise them were not.

One shape is still deliberately unread, and it is written down. Inline
examples inside a paragraph are not detected, and saying so beats implying
otherwise.

Four masks that broke what they promised to protect

  • The optimiser broke email addresses and reported a saving for it. Five of
    ten realistic addresses came out corrupted: please@example.com became
    @example.com, and so did thanks@, basically@, essentially.ops@ and
    very.important@. The politeness, filler and intensifier rules read the local
    part as prose. What is left is not a wrong address, it is not an address.
  • That email mask then shipped with a quadratic quantifier, and CI caught
    what this machine could not: the container is about six times faster than the
    runner, so a 897ms regression measured as comfortable here. RFC 5321's own
    bounds fixed it — 8ms.
  • The rules were rewriting indented code, and the module said so in a
    comment.
    Indentation stripped, keywords sentence-capitalised, and string
    literals edited
    : a SQL clause that matched a different value, a payload
    carrying a different reason. CommonMark's rule that an indented block cannot
    interrupt a paragraph is what makes the fix a specification rather than a
    guess.
  • A trim ran after unmasking, so the last thing to touch a prompt could edit
    what every mask had just promised to leave alone. Found through idempotence
    rather than through a report: 54 of 1,500 corpus inputs.

Four lists nothing held to what they described

The recurring fault of this release, in four places. A list written from the
same list it guards agrees with itself by construction.

  • RECEIPT_LINE_FIELDS, the published list of what leaves a machine, said
    its guard caught a field added to ReceiptLine. It could not: the guard reads
    the keys a receipt emits, and a field populated in one branch — which is the
    shape a leak takes — is on the type and on no emitted line. A plant added one
    and all twelve checks passed.
  • Ten config key lists were second copies of interfaces. A setting added to
    the type and not the list is refused by name; a key on the list the type does
    not have is accepted and never read, which for a budget is a green build for a
    prompt nobody measured.
  • The NUL-byte guard, which is what makes every other guard reviewable,
    walked eleven hand-written directories and missed 289 of 822 source files
    including two published packages, the editor extension and the plugin.
  • The roadmap, above.

All four are bound to what they describe now, five of them as compile errors
rather than tests.

Three documents that stated what was no longer true

  • Nine documents told readers the optimiser would break their email addresses
    and their indented code.
    Both are protected. Every one had been correct when
    it was written.
  • The number the README calls "the entire argument for this tool" was stated
    in three places and checked in one. The drawing was held to the pricing
    catalogue; the alt text, the caption, and the ratio between the two figures
    were not.
  • The README told people to pin an Action two releases old, and the
    release document asked a question the release had already answered.

Two smaller things, recorded rather than dropped

  • One filler family covered two constructions in English where Spanish
    covered five. Corrected, and the wider claim that first accompanied it —
    "the English list was the thinnest in the file" — was measured, found false,
    and narrowed.
  • A measured negative result, kept so nobody re-derives it: an approach that
    looked promising, was tried, and did not work.

What this release did not do

The per-provider price review. docs/releasing.md step 7 asks for the
pricing page of every provider more than 45 days stale to be opened and compared
before a release. Three are: openai, moonshot and xai, at 67 days each. The
session that prepared this release could not reach those pages — its network
policy permits npm, PyPI and Anthropic and nothing else — and a date moved
without looking is the one thing this product refuses absolutely. So the dates
stand where they are, the catalogue's headline date stays 2026-06-24, and every
figure derived from those three providers is reported as stale by the product
itself, which is what that machinery is for.

v1.86.0

Choose a tag to compare

@github-actions github-actions released this 29 Aug 20:55
2c5e907

1.86.0 — "The cheapest place somebody meets this product"

The editor extension, which every roadmap since 0.10.0 has called unblocked and
unscheduled. The reason it stayed unscheduled was never the code: an extension
is a distribution commitment — a marketplace listing, an update cadence, and a
second place where a bug is somebody's afternoon. This arc takes that on
purpose, because the whole problem the ledger names is that almost nobody has
met this product, and a status bar is the cheapest place they could.

What it shows

The token count of the prompt in front of you, the budget that covers the file
and the glob it came from, and what the deterministic rules would actually
recover — measured by running them, never estimated from a ratio. The count
carries its error band, because a token count shown without one reads as exact.

What it refuses

Sending the buffer anywhere, in any form, ever. An extension that uploaded a
prompt in order to price it would be the exact inversion of this product, so the
refusal is a test: every source file in the package is scanned for a way out and
the permitted set is empty. security.test.js permits fetch in the two core
modules that exist to make calls and names them; here there is nothing to name.

A textual scan would miss the interesting version of that failure, so there is a
second check: importing openrouterOverlay or checkedEndpoint from the core
puts a network path in the editor's process without the word fetch appearing
anywhere in this package. Both are forbidden by name.

An editor extension you can test without an editor

A VS Code extension is normally tested by downloading a copy of VS Code and
driving it — a network dependency, a version to keep up with, and a suite that
cannot run on a machine with no display. None of that is compatible with a
product whose argument is that it works offline.

So the split is the design. Every judgement lives in reading.ts, which takes a
string and a config and returns what to show; it has never heard of an editor.
extension.ts is a wire. That the wire stays a wire is a guard rather than an
intention: it performs no arithmetic and formats no figure, and both halves of
that are checked.

The arithmetic check cried wolf three times before it was right, and each
false alarm was an ordinary line. from 'node:fs/promises' read as the division
s/p. import * as vscode read as the multiplication t * a. A guard that
fails on three normal lines is one the next person deletes, which is worse than
never having written it, so it strips comments, then string literals, then the
imports themselves before it looks.

No types package, for the reason 1.85.0 paid to learn

The editor supplies the vscode module at runtime and never installs it, so
@types/vscode would be a dependency that exists only to compile. 1.85.0 spent
three Action jobs on exactly that shape: the CLI typed an optional package with
typeof import(...), tsc resolved it while type-checking, and a package
somebody chooses to install became one this repository could not build without.

src/vscode.d.ts writes out the contract instead — as wide as what the shim
touches and no wider. Widening it is a deliberate edit rather than an inherited
surface.

null is not zero, where it would actually mislead

read runs on every keystroke and optimize walks every rule over the whole
document, so the expensive half waits for the typing to stop. That leaves a
window where the recoverable figure is genuinely unknown, and a status bar
showing 0 in that window would be telling somebody their prompt is already
tight when nobody has checked. It says the rules have not run yet, which is a
third state and a different sentence from the two that look like it.

The packaging was run, not described

npm run package:vscode produces trazum-vscode-1.86.0.vsix: 8 files, 8.39
kB
— the two built modules, the icon, the licence, the README, and the two
manifests vsce writes. No sources, no tests, no source maps. That is
.vscodeignore doing its job, and it is now observed rather than asserted,
which is the difference this repository spent 1.85.0 learning about its own
security headers.

The run found one more thing, which is what running things does. It leaves an
installable file beside the extension, and the guard against runtime state in
this repository checks the tracked tree — correctly, since an untracked file
is a local mess rather than everybody's. But git add -A before a commit takes
whatever the last command left behind, and that is precisely how sixty waiver
records reached main and sat there through two releases. *.vsix is ignored
now, with a test on it, so the documented command cannot cost somebody that.

The wire had no test, and reading it was not the same as running it

The guard on extension.ts asserted that it computes nothing and writes no
status text of its own. Both are true and both are worth having, and both are
assertions about what the file does not do. Nothing anywhere checked that it
does the right thing, and a wire is not a thing without behaviour: it builds a
workspace-relative path for budget matching, it has four separate reasons to
hide the item, it debounces the expensive half, and it has to ignore a document
that is not the one on screen. Every one of those shipped unexercised.

shim.test.js runs activate() for real. The editor is faked to exactly the
surface src/vscode.d.ts declares and resolved through a loader — the same
module.register the web suite uses for next/server, chosen over
--experimental-test-module-mocks for the same reason: a suite should not
depend on a flag a Node minor release can rename. Nothing else is faked. The
core is the core, the reading module is the reading module, and the config is
parsed off a real file on disk.

Eight violations were planted and eight fired, each on the test that claims
it. The one worth describing is the path. The assertion does not watch the call;
it writes a config that scopes prompts/*.txt and checks that a budget appears
in the status bar. That glob can match prompts/support.txt and cannot match
/tmp/…/prompts/support.txt, so a shim that handed over the absolute path shows
a bare token count — and the budget stops applying in the editor while trazum check goes on enforcing it, which is the kind of divergence nobody notices
until they trust the wrong one.

And that guard's first version was bound to its neighbour. It looked for
const path = … and found the first one, which is the config path inside
projectConfig, and reported the shim as broken. Bounding an assertion by its
subject rather than by whatever is nearest is this repository's most-broken rule,
and it broke here, in the test written to hold a different rule.

The fake is the only editor this repository will ever run against

That is the deliberate consequence of not downloading VS Code, and it has a
cost: nothing in the toolchain checks the fake against the declaration. The
declaration is hand-written TypeScript, the fake is plain JavaScript behind a
loader, and tsc never sees them together. A fake that lost a member, or took
fewer arguments than the editor passes, or grew one VS Code does not have, would
leave the whole shim suite green about an editor nobody ships.

contract.test.js holds it in both directions: every declared member is
implemented with the declared arity, and nothing is implemented that is not
declared. It also holds the declaration to being no wider than the shim needs,
because a surface nothing uses is the install-only dependency this whole
approach exists to avoid, growing back one member at a time.

The arity rule was too loose in its first form — anything from the required
count upward — and the plant proved it: a fake that quietly dropped the optional
priority argument passed the check and failed only by accident, on an unrelated
error. It requires the declared count exactly now. A fake cannot observe an
argument it does not take, so a test can never ask where the item was put.

Four comments pointed at guards that were not there

This repository cross-references its own tests constantly, and the reference is
load-bearing prose. "security.test.js permits fetch only in the two modules
that exist to make calls"
is how a reader learns a rule is held rather than
intended. A reference to a file that is not in the tree is worse than no
reference at all: it reports a guard where there is none, and the reader stops
looking.

Four were wrong, and each was a different fault. Two source files and one
declaration named the two guards described above — files promised in a shipped
release and never written, so the extension's wire had no behavioural test and
the comments beside it said otherwise. memory.ts named a postgres suite under
a name it does not have: a rename that took the file and left the sentence. And
draw-icon.mjs named an icon suite that has never existed under any name.

named-guards.test.js scans every source file and document in the repository
and fails on a name that is not a file. It names none of those dead references
in the form it forbids, and neither does its plant — the first version failed on
its own comment, and a guard that has to exempt itself is a guard with a hole
shaped like itself.

The icon claimed a palette it two-thirds had

The last of those four was not only a broken link. draw-icon.mjs says the
colours are "the product's own, taken from docs/assets/demo.svg", and the
guard that was supposed to hold that sentence held a hardcoded copy of the
accent instead: two copies of the same number, agreeing with each other and with
nothing outside the test.

Bound to the file the generator says it took them from, the ground and the
accent were there. The unspent-cell colour was in no other surface of this
product — invented, and presented as the product's own. It...

Read more

v1.85.0

Choose a tag to compare

@github-actions github-actions released this 29 Aug 09:26
245e4be

1.85.0 — "The tokenizer, and a measurement this repository already had"

1.84.0 did not ship, and the plan said in advance that it would not. It
needed a key for each of four families' own counting endpoints, three of them
never arrived, and the 1.83–2.0 plan wrote down that
the arc would continue at 1.85.0 rather than publish a figure derived from
Claude's tokenizer and let a reader assume it was measured against theirs. That
is what happened. The gap is not renumbered away.

The fourth family turned out not to need a key at all, and that is the more
uncomfortable half of this release.

The measurement that was already here

packages/core/test/fixtures/token-ground-truth.openai.json holds 47 samples
measured against gpt-5 through OpenAI's own API with a real key, on
2026-08-28. band.ts went on answering null for openai, and every report
that touched a GPT model went on telling its reader nobody had measured the
estimator against their tokenizer
.

The answer was 112.4% — the worst of the four measured families, on German
prose. o200k_base packs Latin text far more densely than the estimator's
Claude-calibrated divisors expect, so the heuristic runs more than twice over.

The existing guard could not have caught it. It fails a claim with no fixture
behind it
, which is the overclaiming direction. This one was the opposite: a
fixture with no claim in front of it, a measurement somebody paid an API
bill for, thrown away, with a true-sounding sentence left standing where the
number should have been. That is the same fault wearing the opposite sign, and
it is now a guard of its own — a family whose fixture exists and whose figure is
missing fails the build, unless its fixture is the one that governs the
published band.

@trazum/tokenizer-openai

The optional package the plan's third chapter asked for. Install it and OpenAI
prompts are counted exactly, by OpenAI's own byte-pair ranks. Do not install
it and nothing changes at all: @trazum/core imports nothing from it and the
counter arrives through the TokenCounter seam that has been there since the
beginning.

Against the 47-sample corpus this package reproduces the API's counts 47 out
of 47, to the token
. Two independent instruments — a paid API call and an
offline rank table — agreeing exactly on every sample is what makes the 112.4%
above a measurement rather than a claim.

It refuses a model it has no encoding for. Not a nearby one, not the newest
one, not a default. gpt-5-codex and anything shipped since the rank tables
were written are refused, because a count produced by guessing which table to
use would go out labelled exact, and exact is the strongest word this tool
uses about a number.

It does not claim to know whose a model is. A Claude model refuses here as
unknown-encoding rather than as wrong family: this package holds encodings,
not a catalogue, and deciding which provider owns an id already has one source
of truth.

The dependency rule, spent once and narrowed

Every package in this repository has had zero runtime dependencies, and that is
a security property rather than a packaging preference: a dependency is code
that runs over the user's prompt text with no review from this project.

This package has one, and the rule is not being softened. The exception is a
single named package with a single named dependency, written in one file both
guards read; @trazum/core is asserted separately never to appear in it, so
adding it there is not a one-line edit. And js-tiktoken is held to the
property the rule is actually about — MIT, no network, no filesystem, no
subprocess, no evalread out of the installed source on every run,
because "somebody reviewed it once" is not a guarantee that survives a version
bump.

The reason the exception exists at all is the CI case this whole product is
built around: the rank tables are twenty-two megabytes, and a gate that pulls
twenty-two megabytes into every build is a gate teams turn off
.

An architecture picture that cannot go stale quietly

Every architecture diagram in every repository eventually lies. The code moves,
the picture does not, and because a picture cannot be grepped nobody
notices for a year — so the most confident-looking artefact on a front page
becomes the least true one.

So this one is generated from the code by npm run draw:architecture, and
architecture-image.test.js reads the result: a published package the picture
does not show fails the build, so does the core taking a dependency while the
picture claims none, and so does a third module being added to the network
allowlist without the picture saying so. That last one is the sharpest, because
it is the claim a security reviewer reads first.

It was asked for with mingrammer/diagrams and that library was tried properly
before being ruled out — the reasons are in the generator so the afternoon is
not spent twice. Its nodes are fixed-colour images with the label outside the
shape, so they carry no palette and break under anything longer than a word: the
first render came out 1273×2650 with labels overflowing their clusters. It ships
no icon for OpenAI, Anthropic, OpenTelemetry, LiteLLM, Helicone or LangSmith,
which are the products this tool integrates with. And it is built to show what
connects to what, while the claim worth drawing here is what does not cross a
line
— an absence, which a graph of edges is the wrong shape for.

An optional package that was required to compile

CI refused the first version of this release, and it was right to. The CLI
typed its loader as typeof import('@trazum/tokenizer-openai'), which is a
compile-time dependency: tsc resolves the module while type-checking, so
@trazum/cli could not be built at all unless the optional package had been
built first. It passed here only because it had been. On a clean checkout it is
TS2307, twice.

A package somebody chooses to install had quietly become one this repository
cannot compile without, which is the opposite of what the word optional
promises. So the CLI writes out the small contract it actually relies on and
assembles the specifier from fragments, and a test asserts the real package
still satisfies that contract -- two descriptions of one interface is the shape
that drifts silently, so the drift is what is checked.

The first guard against it listed the forms to forbid: a static import, and
typeof import(...). A plant walked straight past it -- a plain
await import('@trazum/tokenizer-openai') with a literal specifier passes both
and still fails the build, because tsc resolves a literal dynamic import too.
The rule is now the one with no forms to enumerate: the specifier never
appears as a literal in the CLI's code.
All three forms fail it.

The dependency was 90% likely obfuscated, and the shape was changed rather than the alert dismissed

Socket flagged js-tiktoken on the pull request that added it. The reading was
fair: the package's main entry inlines every byte-pair table into one 5.6 MB
file whose longest line is 2.3 million characters. That is a vocabulary
rather than hidden code, but nothing about the file says so, and trust me, it
is only data
is not an argument this repository makes anywhere else.

The import moved to js-tiktoken/lite, which loads the logic and the tables
from different files. The executable code this repository now loads is
chunk-*.js, whose longest line is 160 characters and which reads as ordinary
source; the ranks arrive as data, one module per encoding. security.test.js
asserts the property the scanner was reaching for -- the code this repository
loads is not minified into a single line -- so the answer stops being a
judgement about somebody's heuristic and becomes a check this repository owns.

It is also less work: one rank table is parsed instead of six, and only the
one the model actually uses. That made the counter asynchronous, which is the
honest shape for something that loads a two-hundred-thousand-entry table on
demand; the counting function it hands back stays synchronous, which is what a
caller counting a directory needs.

A test that read the machine, and the guard that had two holes

The gate's own test spawned trazum check with no environment and asserted on
over the limit. On a CI runner, where LANG is unset, that passes. On a
contributor's Mac set to Spanish it reads un crecimiento de 151 tokens supera el límite de 25 and fails — the tool answering correctly, in the reader's
language, and the test insisting on English.

This is the second time. env.mjs exists because seven tests in i18n.test.js
did the same thing for months, and it came with a guard meant to stop the next
one. The guard had two holes and the failure went through both. It read a single
directory, so nothing outside packages/cli/test was ever examined, and it
matched only an environment built inline — passing none at all, which is the
machine itself rather than a copy of it, was invisible to it. Two directories
away, apps/web had grown a sixth hand-rolled copy of the object the guard
exists to make unnecessary, and a seventh was found while closing this.

Both holes are shut. The check now reads every tracked suite in the repository,
and holds every spawn of something this repository built — the CLI, the MCP
server, the preflight — to passing an environment it controls. Only those: a
git ls-files for a list of filenames is a spawn too, and demanding ceremony
from it would make the guard noise instead of a rule. A path put into a shell
script and the script then spawned is still a run of that entry point, which is
exactly the shape that got past the old check, so the derivation follows the
value through the bindings that carry it.

env.mjs now reads the detector's variable list out of its source rather than
importing the compiled module. The import was...

Read more

v1.83.0

Choose a tag to compare

@github-actions github-actions released this 28 Aug 23:09
5ca3317

1.83.0 — "A receipt, and the surfaces that could not disagree"

The release has two halves and they answer different questions. One is a new
command. The other is a set of guards for claims this repository had been making
in public and never checking.

The receipt, and the defect it shipped with

trazum receipt writes a bill's counts with the provenance of every figure
attached, and nothing else. No prompt text, no answers, no file paths, no branch
names, no credentials, no session identifiers -- absent rather than redacted,
because the emitter takes a usage profile and a usage profile has no field that
can hold any of them. receipt-redaction.test.js plants all four in a log, the
session field included, and searches the whole serialised document for each.

It sends nothing anywhere. No endpoint, no key, no retry, no queue: the document
goes to a path you name or to standard output.

Then the first version of it turned out to be wrong in the way this product
exists to find in other people's numbers.
It published the catalogue's input
and output rates beside a total the profiler had computed, and those are not
always the same rates. A model inside a promotional window is billed at the
promotion. One whose long-context tier applied is billed at the tier. And a
cached read is billed at a fraction of input that no published field named at
all
-- the catalogue holds it as a multiplier, which never reached the
document. So the obvious check a stranger would run, tokens times the stated
rate, disagreed with the stated total, and nothing said which to believe.

Every line now carries money: input, cache reads, cache writes and output, as
they were actually apportioned, with usd their sum. The rates stay as
provenance. Dividing a bucket by its own token count recovers the rate that was
really charged, including the cached-read rate, which is the figure a reader
most wants and the one nothing carried. Two write-TTL counts come with it,
because the ratio between the TTLs is not constant across providers and a write
total that has lost the split can only be repriced by guessing. And a new gap
says when a TTL was assumed, because the cheaper rate is the right assumption and
the flattering one, which makes the total a floor rather than a measurement.

The receipt is the 46th command and the last one the surface will grow before
2.0. docs/plan-1.83-2.0.md says where that ends.

Claims that were true and unchecked, which is not the same as true

Four surfaces, and nothing proved they agreed. "One core does the measuring;
four surfaces carry it. They cannot disagree, because they are the same
functions"
is on the landing in five languages, and it is the reason a reader is
expected to believe the figure in their terminal, the one their agent quotes and
the one on the page are the same figure. "The same functions" is an argument, not
a proof: two surfaces can call one core and still disagree if either rounds,
filters, defaults or windows differently. One log and one prompt now go through
all four and the results are compared -- the MCP server over real stdio JSON-RPC
framing, the browser through its own run. Planted three ways: the MCP rounding
differently, the browser counting one call more, the CLI repacking a token count
on the way out.

The offline promise was proven for three commands out of 46. "Your prompts
never leave your machine"
is the sentence the README opens with. It is now
proven for every command, in both locales.

A spend gate passed on a log nobody could read. trazum profile on an
unreadable log answered with a total of zero, and a CI gate comparing zero
against a ceiling passed. A gate that cannot read its input now says so instead
of blessing the build.

Seven commands answered a mistyped path with a syscall. A stack trace where a
sentence belonged.

Prices, dates and figures that had drifted

Sonnet 5 was priced at a number nobody charges, on a timer. Its introductory
rate had an end date; the catalogue kept it past that date.

One review date for seven providers. A single date written across every
provider says all seven pages were read the same afternoon. They were not. Each
provider now carries its own, so looking at a page and finding nothing changed is
recorded as a different event from not looking.

Seven copies of one staleness threshold, and a README that said there was
deliberately no threshold while seven places used one.

Three surfaces counted the commands by hand and were wrong. The landing's
headline figure was not one the product printed. The picture at the top of the
README was arithmetic nothing checked. Two figures on the landing were stale in
five languages each. All of them are derived and guarded now.

The web surfaces, which were the least finished thing here

The Playground's terminal was a paper-coloured card. The Write panel opened with
two buttons both reading "Skip this", and the emphasised one skipped. Three
panels printed their own heading twice. A coloured 3px left border was the notice
style in ten places. A disabled primary button looked like a loading one. The
results column scrolled away from the reader editing what it priced. The prompt
editor grew to 900 pixels and pushed its own button off the screen. bg-layer
was a class that painted nothing, so a band meant to separate three beats from
the hero painted nothing at all -- it looked finished in the source and was flat
on the page.

And the landing was blank below the fold, and had been since it shipped.

One more converter, and the tests that had no shape for two shipped defects

trazum from-langsmith reads a LangSmith run export as a usage log, joining
from-litellm and from-helicone.

Two defects reached users because the suite verified prose and behaviour and had
no shape for what it missed. It has one now.

Refactors, for readability rather than for speed. commandProfile was a
single 2,359-line function and the report inside it was 1,200 lines of sequential
printing.

Honesty maintenance

The roadmap promised four things it had already delivered. Under consideration
still said nobody had measured the per-family tokenizer error after 1.82.0
measured it. A guard written for one sentence missed the sentence next to it.
security.test.js's subprocess rule was scoped by directory layout rather than by
intent, so a test directory that had never offended was covered and the first web
test to reach a subprocess was not.

v1.82.0

Choose a tag to compare

@github-actions github-actions released this 27 Aug 12:48
a8e1789

1.82.0 — "The band was a measurement of its own training set"

For eight releases every report Trazum printed said the same thing about every
prompt: ±10%. It was measured, it was committed, a test asserted it, and it
was wrong in a way none of that could catch.

The corpus it was measured on held twenty-one samples: thirteen files of Latin
prose and exactly one each of code, numeric and punctuation
. Those single
files were the set the estimator's constants had been fitted to. So the number
was never a measurement of the estimator. It was a measurement of its own
calibration set, and every dollar figure in the product descended from it.

Twenty-six ordinary samples broke it. Against Anthropic's own counting endpoint,
the same estimator that is 5.6% out on prose is 32.5% out on a CSV ledger.
Telling somebody ±10% about their ledger was telling them a number that is wrong
about their prompt specifically.

There is no single band any more. bandFor(text) answers with one of four
measured figures, and every surface prints the one the text in front of it
earns:

Kind of text Band Worst sample Samples
CJK, all three scripts ±4% 3.2% 6
Latin prose and few-shot blocks ±6% 5.6% 18
Code, markup and quoting ±26% 25.1% 16
Digit-dominant tables and ledgers ±33% 32.5% 7

It deliberately does not classify by text type, and that is the finding rather
than a shortcut: measured by character mix, code and punctuation overlap
completely, and two of the three few-shot samples are indistinguishable from
prose. A classifier over those would be a guess wearing a measurement's name.

The report says how far off it is on a foreign tokenizer, with the number.
The same 47 samples against DeepSeek's own counter are 94.5% out at worst, and
against Mistral's 103.1%. Those two families get the figure on the line; a
family nobody has run is told that nobody has run it.

Hangul was a placeholder nothing had measured, charged han's rate because
the corpus had no Korean in it to say otherwise. Two Korean samples in different
registers agree in lockstep at every candidate, and 1.35 zeroes both: the CJK
class went from 10.6% out to 3.2%.

Two hypotheses were tested and rejected, which is what a bigger corpus buys.
Digit-run length does not predict the numeric error, and neither does
grouped-number density. With two samples each would have been fitted and shipped
as a fix.

trazum from-helicone reads a Helicone request export as a usage log —
the fourth converter, and the one that needs three columns to answer what model
ran. The model that answered is what gets priced, and every substitution is
counted. Forty-four commands.

v1.81.0

Choose a tag to compare

@github-actions github-actions released this 26 Aug 21:23
6a28e55

1.81.0 — "The things nobody had checked"

Nothing here is a new idea. Every item is something that had been in this
repository for weeks or months, that looked finished, and that nobody had ever
opened and read against what it claimed to do. Fourteen of the twenty-two
entries in the changelog are that shape.

The web app was one environment variable away from spending your money on
strangers.
POST /api/optimize fell back to TRAZUM_LLM_API_KEY, then
ANTHROPIC_API_KEY, then to the CLI's providerFromEnv(), whenever a request
carried no key of its own. On a deployment with either variable set, anyone who
posted {"suggest": true} spent the operator's credit: no account, no session,
nothing to attribute the call to, and the rate limiter in front of it keyed on a
header the caller chooses. It was never armed on trazum.vercel.app, which
answered "llmConfiguredOnServer": false the whole time, so nothing was ever
spent. It was a trap set, and it would have armed itself the day somebody
configured a key. The endpoint and the model may still come from the operator;
the key may not.

That same GET stopped publishing llmConfiguredOnServer at all, because an
unauthenticated, unlimited endpoint answering "is there a key here worth
attacking" is an oracle. It also gained the rate limiter that only POST had.

An account you could never close. Accounts arrived in 1.7.0 and there was no
way out of them: deleteUser did not exist in the store interface, in either
driver, or anywhere else. DELETE /api/account closes one now, and takes with it
every session, every prompt, every version of each, and every share link the
account published. Immediate, with no grace period, because a screen that says
deleted should mean deleted. Shared /c/<token> links stop working, which is the
answer rather than an oversight: keeping them would mean keeping the deleted
person's prompt text.

Sign out everywhere, which the store could always do. deleteSessionsForUser
existed in the interface and in both drivers with no caller anywhere, so somebody
whose laptop was stolen could sign out on their phone and the stolen cookie
stayed valid for the rest of its thirty days.

A ten-minute window that only the browser was keeping. The OAuth state cookie
had a maxAge and nothing else, while the callback's own comment described an
expired state as a case it handled. The state carries an issue time now and the
server checks it. It is deliberately not signed, and the code says why: anyone
who can write that cookie can ask for a fresh one, so a forged timestamp buys an
attacker nothing. An HMAC would have looked like it closed something.

The one auth route nothing bounded. GET /api/auth/session ran a database
lookup on every call, for an unauthenticated caller, with a cookie that caller
chose, while the limiter for the sign-in routes sat exported twenty lines away in
the same file. It has its own budget rather than sharing that one, because the
header polls this endpoint and sharing would refuse somebody at the moment they
pressed sign in.

Sessions that expired and were never swept. The lookup reaped the row it was
handed, which covers a session somebody comes back to. Nobody was reaping the
ones nobody comes back for, and those are the majority. Not a way in, since an
expired row cannot authenticate anybody; unbounded growth in a table whose rows
are all dead weight after thirty days.

And two things that are new

A Claude Code status line that costs nothing. It shows what the session has
spent, in Trazum's numbers rather than an estimate, and it is free because of
where it runs: the status line's output is drawn in the terminal and a Stop
hook's goes to the debug log, so neither is context and neither is billed.
SessionStart is the hook whose output is context, and a test refuses it by
name.

trazum from-claude-code --state reads only what a transcript appended since
last time. On the largest real transcript on one machine, 212 MB, the conversion
drops from 2.6s to 0.19s, and the records it appends are byte for byte what a
full read would have produced.

The part worth saying about how this was built

Four guards were planted and did not fail. Each time the test was what was
wrong, not the code. A test that checked one account could not revoke another's
sessions passed just as happily against a route that grew a ?user= parameter;
it attempts the attack now. A test that checked account deletion removed a
prompt could not see the orphaned version rows left behind, because no public
call can reach a version without its prompt; and the count added to make it
visible summed what the loop was about to delete, so it reported success with
the delete removed. A number that fails in the same direction as the code it
reports on is not evidence.