@trazum/core
v1.86.0
Published
Trazum core: priced advisories for LLM prompts (caching, model tier, batching, schemas), plus deterministic trimming, token counting and pricing.
Maintainers
Readme
@trazum/core
Prompt optimiser. Shortens what you send to a model without changing what you ask for, and tells you what that is worth per month.
Zero runtime dependencies, and that is asserted in CI rather than promised. This library reads your prompts; every dependency would be someone else's code reading them too.
Part of Trazum. For the terminal, see
@trazum/cli.
npm install @trazum/coreUse
import { optimize } from '@trazum/core';
const result = optimize(prompt, {
level: 'safe', // or 'aggressive' — read the diff
usage: {
model: 'claude-opus-5',
callsPerMonth: 50_000,
avgOutputTokens: 500,
cacheHitRate: 0.9,
batchEligible: false,
},
});
result.optimized; // the shortened prompt
result.savings.monthlySavingsUsd; // what that is worth
result.advisories; // usually where the real money is
result.rules; // what each rule changed, with samplesThe optimisation is deterministic: same input, same output, no API key, no network call, nothing to pay.
What it does
Trims with deterministic rules — courtesy, filler, verbose phrasing, duplicated paragraphs, decorative separators, shouting in capitals.
Never touches what would break the prompt. Code fences, inline code, URLs,
template placeholders ({{x}}, ${x}, {x}, {% %}) and XML/HTML tags are
isolated before any rule runs. If a rule ever did make one of those disappear,
that rule is discarded and the rest carry on.
Tells you where the money actually is. Trimming is usually not it. Read
result.advisories first — they are sorted warnings-first, then by monthly
saving:
| Advisory | What it finds |
|---|---|
| prompt-caching | Caching is a byte-for-byte prefix match, so a template only caches up to its first placeholder. The saving is computed over the real stable prefix. |
| cache-prefix-reorder | Stable instructions stranded after the first placeholder never cache. Often the single largest saving available. |
| contradictory-instructions | "Answer in English" three paragraphs above "reply in the customer's own language". A correctness bug that also costs tokens twice. |
| redundant-examples | Few-shot examples that are near-copies of an earlier one. |
| restated-output-format | A schema shown in a code block and then walked again in prose. |
| movable-output-schema | A schema the request could carry as a parameter instead — cheaper and stricter, and the one finding that is not a trade-off. Reported, never edited: it changes the call, not the prompt. |
| model-downgrade | A keyword heuristic, not a quality judgement — validate with evaluations before acting on it. |
| output-dominated | You pay more for the answer than the prompt, so shortening it has a ceiling. |
| batch-api, below-cache-minimum, context-window, promotional-pricing | |
The last three structural findings are advisory only. A contradiction has a right answer only you know, and an example that looks redundant may be demonstrating a boundary case on purpose. Trazum points; it does not cut.
Measured usage
measuredUsage(report, label) turns one label's slice of a profileUsage()
report into the UsageProfile that optimize() multiplies by — the real call
count, output size and cache-read share instead of typed guesses — with the
provenance attached: the span, the scaling factor when a month was derived
(never under seven days of data), the chosen model's share when the label ran
on several, and whether output was ever recorded. labelCoverage(report,
labels) names both mismatches between a config's label→prompt map and the
log: prompts mapped to labels with no traffic, and labels carrying money with
no prompt mapped.
The fleet
assignSources(files, sources) assigns log files to named services by the
most specific matching glob — the budget patterns' own precedence rule — and
returns the files matching no source rather than dropping them.
fleetRollup(sources) sums the fleet, names the dearest source with its
share, flags spans too different to compare as rates, finds the same label
running on different models in different sources (judged on each source's
dearest model, so a stray experiment is not a migration), and names sources
where caching loses money while the aggregate pays off.
The plan
buildPlan(report, levers, pricingLastReviewed) turns a report and its
levers into a ranked PlanDocument: route and batch on one slice arrive as a
single pre-combined action (combinedUsd, never a sum), the truncation
action's stake is the measured retry bill, and a cache action exists only for
a settled loss — an unsettled verdict is a missing field, and "add the field"
is the report's advice, not a plan's. Projected savings and measured stakes
are totalled separately; every action carries typed assumptions
(PlanAssumption) the log cannot confirm, as data rather than prose, so the
renderer localizes them and a later verification can match them
structurally. planLabelName renders the unlabelled bucket without leaking
the sentinel.
Did it work?
verifyPlan(plan, newerReport, { currentPricingLastReviewed }) holds a saved
plan to the report of a later log, with three outcomes and never two:
arrived, not-arrived, or cannot-tell — the workload vanished, the
fields the detection needs stopped being recorded, or tokens cannot say
which tier billed them. Differences carry the world's measured movement from
the plan's recorded baseline (calls, tokens per call, both numbers and never
a verdict), a repricing between the two documents is flagged rather than
silently priced through, and each verdict says whether it fails a gate:
not-arrived always, fields-stopped too — a team must not pass on the
strength of its own log's silence — and a vanished workload never.
The long run
buildHistory(reports, plans) turns stored reports into the series no
pairwise comparison can see: per-label spend, per-model share and cache
share per period, with consecutive movement named as runs (MIN_RUN rises
or falls at least — two is a comparison, one is noise) and the same plan
action appearing in two or more plans reported as a decision nobody is
executing. storedReportFrom(name, parsed) reads one stored profile
document into the slice history needs, returning null for anything else so
the caller names the file instead of absorbing it. No forecasts anywhere:
shapes are stated with their first and last values, and where they go next
is the reader's.
The bill, from the provider
normalizeAnthropicUsage(payload) and normalizeOpenAIUsage(payload) turn a
usage API response into UsageBuckets — token sums per window and model, with
the two cache-write TTLs kept apart and a request count only where the
provider actually serves one (null otherwise, never zero).
bucketedProfile(pull, { catalogue }) prices them, and
bucketedCacheEconomics(report) runs the same counterfactual cacheEconomics
runs per call. The result is deliberately its own shape rather than a
UsageProfileReport with holes in it, so no per-call finding can read a zero
this module wrote: every finding a sum cannot support is listed in
unavailable with why and what would unlock it. Anything unreadable in the
payload becomes a named PullGap, never a default of zero. Pure and
browser-safe — the fetch, the credentials and the pagination live in the CLI.
The bill, from what is already on disk
claudeCodeRecords(text, { label }) converts a Claude Code transcript into
usage-log records — the token counts and the model cross, the words never do,
and that absence is asserted by test rather than promised.
looksLikeClaudeCodeTranscript(text) detects one, so a mixed folder routes
itself. otelRecords(text) does the same for an OpenTelemetry export's GenAI
spans, and returns what it skipped beside what it converted — "1 LLM span
priced, 1 other ignored" is a different sentence from a bare total. Both are
pure and browser-safe; the web app's Bill tab runs them on a dropped folder.
Should we switch models?
switchAnalysis(report, targetId, { catalogue, on, migrationUsd, evalCases })
prices the decision a what-if only gestures at: the saving on the same tokens
(the sign is proven in both directions — a switch that costs money says so),
the break-even of a declared migration cost as division over the measured
days — refused by name (no-saving, no-clock) rather than approximated —
and the cost of the evaluation itself, three provider calls per case. Quality
is never in the result: the analysis defers to route, every time.
ownRate({ gpuUsdPerHour, tokensPerSecond, utilization }) derives a
self-hosted $/MTok from your declared numbers — the one price source here
that is neither published nor bundled: it is your own measurement, and the
function refuses rather than defaults when a number is missing or out of
range.
The store
resolveStore(records) collapses an append-only log into the current truth:
one record per identity — provider, window, model, grouping — with the later
pull winning, so overlapping pulls converge instead of double-counting.
Records it cannot tell apart are returned as possiblyDouble rather than
merged, and lines from a newer schema are counted rather than dropped.
recordsFromBuckets and bucketsFromRecords round-trip a pull through disk
so a stored month prices exactly as a fresh one does; storeInventory
summarises what is held; pruneRecords returns what a retention cutoff would
remove, with its span, because silence about deleted measurements is the one
thing a store must not do.
Watching
evaluateWatch({ report, thresholds, nowMs, ... }) decides what has crossed.
Crossings are measured and carry provenance as a field, so a later change
cannot smuggle an estimate past a consumer by staying silent. A period too
short to judge becomes an abstention rather than a pass — three states, never
two — and a crossing already reported comes back as suppressed rather than
vanishing, because "we alerted about this" and "this is fine now" are
different sentences. The unwatched stretch between cycles is returned as
gap.
Answering before the call
answerCost(request, { catalogue }) answers the two questions asked at call
time. The budget consumed is measured, the cost of the described call is
estimated, and the two never merge: the composed afterCall.usd carries its
halves beside it, and restsOn says whether the verdict needed the estimate
(measured when the budget is already past its limit, measured+estimated
when it takes this call to cross). Three outcomes rather than two — within,
over, cannot-tell — and the reasons are kept apart because their fixes
differ: no budget configured, nothing measured, or a model the catalogue
cannot price. Pure and synchronous, because a function that reads a file
cannot promise an answer in milliseconds.
The guard
guardSpend(request, { catalogue }) wraps answerCost with what to do
instead. A refusal carries alternatives — route, batch, or both combined the
way billLevers combines them rather than summed — each priced for the single
call being decided, each carrying its typed assumption, and each filtered to
models whose context window the prompt actually fits. cannot-tell never
becomes yes: a guard that permits whatever it cannot judge permits
everything the moment its inputs go missing.
Comparing two versions
import { comparePrompts, formatSignedUsd } from '@trazum/core';
const change = comparePrompts(before, after, { usage });
change.tokenDelta; // +37 (grew)
formatSignedUsd(change.monthlyDeltaUsd); // "+$9.25"
change.advisories.appeared; // what the edit brokeNote the sign. Everything comparePrompts returns is after - before, so
positive means worse — the opposite of result.savings. That is why it lives
in its own module and why nothing in it is called a saving.
Prices
The bundled catalogue is correct as of PRICING_LAST_REVIEWED and needs no
setup. When a published price moves, correct it without upgrading this package:
import { catalogueFromOverlay, optimize } from '@trazum/core';
const pricing = catalogueFromOverlay(`{
"lastReviewed": "2027-01-15",
"models": { "claude-opus-5": { "inputPerMTok": 6 } }
}`);
const result = optimize(prompt, { usage, pricing });
result.pricingSource; // says which models came from the overlay, and its dateA catalogue is a value: applying an overlay returns a new one and mutates nothing, so your prices never leak into another caller's report.
openrouterOverlay(payload, { knownIds, lastReviewed }) turns an OpenRouter
/models response into that same overlay shape — models you already price are
left alone, and anything unreadable comes back in skipped with a reason
rather than vanishing. It is pure, so the browser can run it on a pasted
response; the CLI's --pricing-live is this function behind a fetch.
Two entry points
@trazum/core imports no Node builtins — enforced by a test that walks the
import graph, because the browser bundles it. Anything that reads the filesystem
lives on @trazum/core/node:
import { loadConfig, walkPrompts } from '@trazum/core/node';Optional LLM pass
For the compression rules cannot do. The result is only accepted if it is shorter and leaves protected content byte-identical — it never returns something worse than where it started.
import { openAiCompatible, refineWithLlm } from '@trazum/core';
const refined = await refineWithLlm(result, openAiCompatible({
baseUrl: 'https://your-llm/v1',
apiKey: process.env.LLM_KEY,
model: 'your-model',
}));Token counts
The bundled estimator is a dependency-free heuristic, and it compares two versions of the same prompt well — which is what it is for. Its error band is measured per kind of text, because one band for all text was a measurement of its own calibration set rather than of the estimator:
| 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 |
Call bandFor(text) to get the band for a specific prompt. ESTIMATE_ERROR_BAND_PCT
is still exported and is the widest of the four, for callers that have to print one
figure without holding the text.
That band is measured, against the official counting endpoint, over 47 samples in ten languages and 6 text types. The margin between each bucket and its own worst sample is a point or less, and no bucket is allowed to overlap the next: a sample sorted into a friendlier bucket gets a smaller band and fails the test.
The single band of 10% this replaced was an unchecked design target for eight releases and it was false; the 1.9.0 entry in the changelog says what was wrong, and the 1.82.0 entry says what extending the corpus from 21 samples to 47 found.
Three things follow from how it was measured, and they matter if you depend on the number:
- The band is a Claude number, and the others are far outside it. The same 47
samples measured against DeepSeek's own counter came out 94.5% wrong at
worst, and against Mistral's 103.1% — Trazum prices 7 providers and the
band belongs to one.
foreignTokenizeranswers with the provider id whenever the model is not Anthropic's, and the report says so on the line rather than printing a figure that was measured somewhere else. - It detects the language and divides accordingly, because one divisor
calibrated on English measured −37.3% on German.
detectTextLanguageanswersnullwhen it cannot tell, which falls back to the English behaviour. - Kana, han and hangul cost different amounts. Charging one token per CJK character put Japanese at +11.2% and Chinese at −3.2%; kana measure 0.75 tokens per character, han 1.05 and hangul 1.35. Hangul had been carrying a placeholder nothing measured until two Korean samples agreed in lockstep on 1.35, which took the CJK bucket's worst error from 10.6% to 3.2%.
For exact figures, pass a real counter — the counting endpoint is free:
import { countTokensAnthropic, withExactTokenCounts } from '@trazum/core';
const exact = await withExactTokenCounts(result, countTokensAnthropic({ apiKey }));Locales
English and Spanish. A locale changes the report, never the optimisation — the same prompt in any locale produces the same optimised text, the same token counts and the same advisory ids. Match on ids, not on prose.
Versioning
See VERSIONING.md. Rule and advisory identifiers are stable across locales and versions on purpose; message text is copy and gets improved.
MIT.
