@trazum/cli 1.50.8 → 1.50.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +63 -0
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/es.d.ts.map +1 -1
- package/dist/i18n/es.js +63 -0
- package/dist/i18n/es.js.map +1 -1
- package/dist/i18n/types.d.ts +33 -0
- package/dist/i18n/types.d.ts.map +1 -1
- package/dist/index.js +207 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/i18n/en.ts +78 -0
- package/src/i18n/es.ts +78 -0
- package/src/i18n/types.ts +30 -0
- package/src/index.ts +273 -0
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import { open, readdir, readFile, stat, writeFile } from 'node:fs/promises';
|
|
|
4
4
|
import { dirname, join, resolve as resolvePath } from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { gunzipSync } from 'node:zlib';
|
|
7
|
-
import { applyRewrites, BASELINE_FILENAME, BASELINE_VERSION, breaches, cacheableMinimum, analyzeCachePrefix, billLevers, bucketedCacheEconomics, bucketedProfile, buildHistory, buildPlan, connectorFor, CONNECTORS, normalizeAnthropicUsage, normalizeOpenAIUsage, bucketsFromRecords, evaluateWatch, firedKey, pruneRecords, recordsFromBuckets, storeInventory, storedReportFrom, verifyPlan, cacheEconomics, cacheHitRate, contextPressure, comparePrompts, compareToBaseline, computeSavings, countTokensAnthropic, DEFAULT_USAGE, budgetPositions, conform, BREAK_EVEN_BAND, runExperiment, qualityGate, ladderPosition, validateLadder, outcomeReport, rankPerOutcome, FAILURE_POLICIES, detectFromSource, matchLocale, parsePlanDocument, waiverDay, waiverHistory, proposeInit, MIN_RATE_DAYS, parseConfig, coverageDrift, driversBetween, explainGateFailure, assignSources, fleetRollup, labelCoverage, measuredUsage, gateMargin, GATE_MARGIN_TIGHT, estimateTokens, evaluate, extractPrompts, findExamples, formatBaseline, formatSignedUsd, formatUsd, getMessages, getModel, hasMarker, LOCALES, MAX_BASELINE_BYTES, moneyIsComparable, mostSpecificMatch, nearestName, optimize, parseBaseline, PHRASE_LANGUAGES, plannedCalls, profilePrompt, profileToCsv, profileUsage, promptId, providerFromEnv, pruneExamples, refineWithLlm, rejectionText, reorderForCache, repriceProfile, reviewAgeDays, reviewExamples, RULES, sharedPrefixes, sharesOf, SOURCE_EXTENSIONS, suggestRewrites, toOtlpMetrics, toPromptfoo, TTL_1H_MS, UNLABELLED, withExactTokenCounts, } from '@trazum/core';
|
|
7
|
+
import { applyRewrites, BASELINE_FILENAME, BASELINE_VERSION, breaches, cacheableMinimum, analyzeCachePrefix, billLevers, bucketedCacheEconomics, bucketedProfile, buildHistory, buildPlan, connectorFor, CONNECTORS, normalizeAnthropicUsage, normalizeOpenAIUsage, bucketsFromRecords, evaluateWatch, firedKey, pruneRecords, recordsFromBuckets, storeInventory, storedReportFrom, verifyPlan, cacheEconomics, cacheHitRate, contextPressure, comparePrompts, compareToBaseline, computeSavings, countTokensAnthropic, DEFAULT_USAGE, budgetPositions, conform, BREAK_EVEN_BAND, allocate, runExperiment, qualityGate, semanticPassCost, verifySemanticProposals, SEMANTIC_SYSTEM_PROMPT, ladderPosition, validateLadder, outcomeReport, rankPerOutcome, FAILURE_POLICIES, detectFromSource, matchLocale, parsePlanDocument, waiverDay, waiverHistory, proposeInit, MIN_RATE_DAYS, parseConfig, coverageDrift, driversBetween, explainGateFailure, assignSources, fleetRollup, labelCoverage, measuredUsage, gateMargin, GATE_MARGIN_TIGHT, estimateTokens, evaluate, extractPrompts, findExamples, formatBaseline, formatSignedUsd, formatUsd, getMessages, getModel, hasMarker, LOCALES, MAX_BASELINE_BYTES, moneyIsComparable, mostSpecificMatch, nearestName, optimize, parseBaseline, PHRASE_LANGUAGES, plannedCalls, profilePrompt, profileToCsv, profileUsage, promptId, providerFromEnv, pruneExamples, refineWithLlm, rejectionText, reorderForCache, repriceProfile, reviewAgeDays, reviewExamples, RULES, sharedPrefixes, sharesOf, SOURCE_EXTENSIONS, suggestRewrites, toOtlpMetrics, toPromptfoo, TTL_1H_MS, UNLABELLED, withExactTokenCounts, } from '@trazum/core';
|
|
8
8
|
import { cacheDir, cacheStats, cachingProvider, clearCache } from './suggest-cache.js';
|
|
9
9
|
import { dayOf, formatGap, median, spanDays } from './time.js';
|
|
10
10
|
// Everything that reads the filesystem, on its own entry point so the web
|
|
@@ -326,6 +326,8 @@ const COMMAND_FLAGS = {
|
|
|
326
326
|
ladder: ['pricing', 'pricing-live', 'since', 'until', 'label'],
|
|
327
327
|
experiment: ['a', 'b', 'min-outcomes', 'pricing', 'pricing-live'],
|
|
328
328
|
quality: ['label', 'at', 'gate', 'pricing', 'pricing-live'],
|
|
329
|
+
semantic: ['yes', 'model', 'pricing', 'pricing-live'],
|
|
330
|
+
owners: ['pricing', 'pricing-live', 'since', 'until'],
|
|
329
331
|
where: [],
|
|
330
332
|
rules: [],
|
|
331
333
|
blame: ['limit', 'model', 'calls', 'output-tokens', 'batch', 'prompt', 'markdown-out'],
|
|
@@ -1966,6 +1968,204 @@ async function commandQuality(args, config, pricing, t) {
|
|
|
1966
1968
|
}
|
|
1967
1969
|
console.log();
|
|
1968
1970
|
}
|
|
1971
|
+
/**
|
|
1972
|
+
* `trazum semantic <prompt> [--yes]` — the findings a dictionary cannot see.
|
|
1973
|
+
*
|
|
1974
|
+
* The rules engine has deferred these since 0.1.0 for one honest reason: a
|
|
1975
|
+
* dictionary cannot see meaning, and a model that hallucinates a finding is
|
|
1976
|
+
* worse than a rule that misses one.
|
|
1977
|
+
*
|
|
1978
|
+
* **The price is printed before anything is sent, and `--yes` is required.** A
|
|
1979
|
+
* tool that spends somebody's money to tell them how to spend less has to be
|
|
1980
|
+
* the first thing audited by its own arithmetic, and it has to ask.
|
|
1981
|
+
*/
|
|
1982
|
+
async function commandSemantic(args, config, pricing, t) {
|
|
1983
|
+
const prompt = await readInput(args.positional[0], t);
|
|
1984
|
+
const modelId = stringFlag(args, 'model') ?? config.usage?.model ?? DEFAULT_USAGE.model;
|
|
1985
|
+
const model = pricing.byId.get(modelId) ?? getModel(DEFAULT_USAGE.model);
|
|
1986
|
+
const rates = { inputPerMTok: model.inputPerMTok, outputPerMTok: model.outputPerMTok };
|
|
1987
|
+
const cost = semanticPassCost(prompt, rates);
|
|
1988
|
+
const n = (value) => value.toLocaleString(t.numberLocale);
|
|
1989
|
+
console.log();
|
|
1990
|
+
console.log(c.bold(t.semantic.heading(args.positional[0] ?? '-')));
|
|
1991
|
+
console.log();
|
|
1992
|
+
console.log(` ${wrap(t.semantic.willCost(formatUsd(cost.usd), n(cost.inputTokens), n(cost.outputTokens), model.displayName), 74, ' ')}`);
|
|
1993
|
+
if (!boolFlag(args, 'yes')) {
|
|
1994
|
+
// Nothing has been sent at this point, and nothing will be. The price
|
|
1995
|
+
// above is the whole output of a run without --yes.
|
|
1996
|
+
console.log();
|
|
1997
|
+
console.log(` ${c.dim(t.semantic.needsYes())}`);
|
|
1998
|
+
console.log();
|
|
1999
|
+
return;
|
|
2000
|
+
}
|
|
2001
|
+
const provider = providerFromEnv();
|
|
2002
|
+
if (!provider)
|
|
2003
|
+
throw new Error(t.errors.llmNotConfigured());
|
|
2004
|
+
const answer = await provider.complete({ system: SEMANTIC_SYSTEM_PROMPT, user: prompt });
|
|
2005
|
+
let proposals = [];
|
|
2006
|
+
try {
|
|
2007
|
+
const parsed = JSON.parse(/^(?:```|~~~)[a-zA-Z]*\n([\s\S]*?)\n?(?:```|~~~)$/.exec(answer.trim())?.[1] ?? answer.trim());
|
|
2008
|
+
/**
|
|
2009
|
+
* A response that is not the shape asked for is **no proposals**, never a
|
|
2010
|
+
* crash and never a partial read. The model was told exactly what to
|
|
2011
|
+
* return; anything else is a response this layer cannot check, and an
|
|
2012
|
+
* unchecked finding is the one thing this whole module exists to prevent.
|
|
2013
|
+
*/
|
|
2014
|
+
if (Array.isArray(parsed)) {
|
|
2015
|
+
proposals = parsed.filter((entry) => typeof entry === 'object' &&
|
|
2016
|
+
entry !== null &&
|
|
2017
|
+
Array.isArray(entry.spans) &&
|
|
2018
|
+
entry.spans.length === 2 &&
|
|
2019
|
+
entry.spans.every((span) => typeof span === 'string'));
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
catch {
|
|
2023
|
+
proposals = [];
|
|
2024
|
+
}
|
|
2025
|
+
const result = verifySemanticProposals(prompt, proposals);
|
|
2026
|
+
const lineOf = (offset) => prompt.slice(0, offset).split('\n').length;
|
|
2027
|
+
console.log();
|
|
2028
|
+
if (result.findings.length === 0) {
|
|
2029
|
+
console.log(` ${c.dim(t.semantic.nothingFound())}`);
|
|
2030
|
+
}
|
|
2031
|
+
for (const finding of result.findings) {
|
|
2032
|
+
console.log(` ${c.bold(t.semantic.finding(finding.kind, finding.because))}`);
|
|
2033
|
+
finding.spans.forEach((span, index) => {
|
|
2034
|
+
const shown = span.length > 90 ? `${span.slice(0, 87)}…` : span;
|
|
2035
|
+
console.log(` ${c.dim(t.semantic.span(String(lineOf(finding.offsets[index] ?? 0)), shown))}`);
|
|
2036
|
+
});
|
|
2037
|
+
console.log(` ${c.dim(wrap(finding.ceilingTokens > 0 ? t.semantic.ceiling(n(finding.ceilingTokens)) : t.semantic.noCeiling(), 70, ' '))}`);
|
|
2038
|
+
console.log();
|
|
2039
|
+
}
|
|
2040
|
+
/**
|
|
2041
|
+
* What did **not** survive, counted and reasoned.
|
|
2042
|
+
*
|
|
2043
|
+
* A pass that showed only its accepted findings would hide its own hit
|
|
2044
|
+
* rate, and the hit rate is the most useful thing a reader can know about
|
|
2045
|
+
* whether to run it again.
|
|
2046
|
+
*/
|
|
2047
|
+
if (result.rejected.length > 0) {
|
|
2048
|
+
console.log(` ${c.dim(t.semantic.rejected(n(result.rejected.length)))}`);
|
|
2049
|
+
for (const { proposal, reason } of result.rejected.slice(0, 5)) {
|
|
2050
|
+
const span = proposal.spans[0];
|
|
2051
|
+
const shown = span.length > 50 ? `${span.slice(0, 47)}…` : span;
|
|
2052
|
+
console.log(` ${c.dim(t.semantic.rejectedLine(reason, shown))}`);
|
|
2053
|
+
}
|
|
2054
|
+
console.log();
|
|
2055
|
+
}
|
|
2056
|
+
console.log(` ${c.dim(wrap(t.semantic.disposes(), 74, ' '))}`);
|
|
2057
|
+
console.log(` ${c.dim(wrap(t.semantic.optIn(), 74, ' '))}`);
|
|
2058
|
+
console.log();
|
|
2059
|
+
}
|
|
2060
|
+
/**
|
|
2061
|
+
* `trazum owners <log>` — whose budget each workload lands on.
|
|
2062
|
+
*
|
|
2063
|
+
* The fleet answered *which service* in 1.37. This answers *whose money*,
|
|
2064
|
+
* which is the question that decides whether anything on the list gets done: a
|
|
2065
|
+
* report saying "the bill is $40,000 and here is $9,000 of savings" is read by
|
|
2066
|
+
* four people who each assume it is one of the other three's problem.
|
|
2067
|
+
*
|
|
2068
|
+
* **The unallocated is its own line and is never spread.** See `owners.ts` for
|
|
2069
|
+
* why that is worth breaking a module over.
|
|
2070
|
+
*/
|
|
2071
|
+
async function commandOwners(args, config, pricing, t) {
|
|
2072
|
+
const path = args.positional[0];
|
|
2073
|
+
if (path === undefined)
|
|
2074
|
+
throw new Error(t.errors.missingInputFile());
|
|
2075
|
+
console.log();
|
|
2076
|
+
console.log(c.bold(t.owners.heading()));
|
|
2077
|
+
if (config.owners === undefined) {
|
|
2078
|
+
console.log(` ${c.dim(wrap(t.owners.noOwners(), 74, ' '))}`);
|
|
2079
|
+
console.log();
|
|
2080
|
+
return;
|
|
2081
|
+
}
|
|
2082
|
+
const report = profileUsage(await readUsageLog(path, t), { catalogue: pricing });
|
|
2083
|
+
const result = allocate(report.byLabel.map((entry) => ({
|
|
2084
|
+
label: entry.label,
|
|
2085
|
+
usd: entry.breakdown.totalUsd,
|
|
2086
|
+
calls: entry.breakdown.calls,
|
|
2087
|
+
})), config.owners);
|
|
2088
|
+
const n = (value) => value.toLocaleString(t.numberLocale);
|
|
2089
|
+
const pct = (value) => `${(value * 100).toFixed(1)}%`;
|
|
2090
|
+
/**
|
|
2091
|
+
* Problems first, before any figure.
|
|
2092
|
+
*
|
|
2093
|
+
* A split that does not sum to one sends a whole workload to unallocated,
|
|
2094
|
+
* and a reader who saw the table before the explanation would go looking for
|
|
2095
|
+
* a bug in their logs.
|
|
2096
|
+
*/
|
|
2097
|
+
if (result.problems.length > 0) {
|
|
2098
|
+
console.log();
|
|
2099
|
+
console.log(` ${c.red('✗')} ${c.bold(t.owners.problemsHeading())}`);
|
|
2100
|
+
for (const problem of result.problems) {
|
|
2101
|
+
const detail = problem.kind === 'budget-for-unknown-owner'
|
|
2102
|
+
? problem.owner
|
|
2103
|
+
: problem.kind === 'split-does-not-sum'
|
|
2104
|
+
? `"${problem.label}" sums to ${problem.total}`
|
|
2105
|
+
: problem.kind === 'negative-share' || problem.kind === 'split-names-unknown-owner'
|
|
2106
|
+
? `"${problem.label}" → ${problem.owner}`
|
|
2107
|
+
: `"${problem.label}"`;
|
|
2108
|
+
console.log(` ${wrap(t.owners.problem(problem.kind, detail), 70, ' ')}`);
|
|
2109
|
+
}
|
|
2110
|
+
process.exitCode = 1;
|
|
2111
|
+
}
|
|
2112
|
+
console.log();
|
|
2113
|
+
const col = t.owners.columns;
|
|
2114
|
+
const rows = result.owners.map((line) => ({
|
|
2115
|
+
owner: line.owner,
|
|
2116
|
+
spend: formatUsd(line.usd),
|
|
2117
|
+
budget: line.budgetUsd === null ? '—' : formatUsd(line.budgetUsd),
|
|
2118
|
+
calls: n(Math.round(line.calls)),
|
|
2119
|
+
verdict: t.owners.verdict(line.verdict),
|
|
2120
|
+
kind: line.verdict,
|
|
2121
|
+
}));
|
|
2122
|
+
const w = {
|
|
2123
|
+
owner: Math.max(...rows.map((r) => r.owner.length), col.owner.length),
|
|
2124
|
+
spend: Math.max(...rows.map((r) => r.spend.length), col.spend.length),
|
|
2125
|
+
budget: Math.max(...rows.map((r) => r.budget.length), col.budget.length),
|
|
2126
|
+
calls: Math.max(...rows.map((r) => r.calls.length), col.calls.length),
|
|
2127
|
+
};
|
|
2128
|
+
console.log(c.dim(` ${col.owner.padEnd(w.owner)} ${col.spend.padStart(w.spend)} ` +
|
|
2129
|
+
`${col.budget.padStart(w.budget)} ${col.calls.padStart(w.calls)}`));
|
|
2130
|
+
for (const row of rows) {
|
|
2131
|
+
const tint = row.kind === 'over' ? c.red : row.kind === 'not-measured' ? c.yellow : c.dim;
|
|
2132
|
+
console.log(` ${row.owner.padEnd(w.owner)} ${row.spend.padStart(w.spend)} ` +
|
|
2133
|
+
`${row.budget.padStart(w.budget)} ${row.calls.padStart(w.calls)} ${tint(row.verdict)}`);
|
|
2134
|
+
}
|
|
2135
|
+
// The 1.37 refusal, applied to people, said in full for each owner it hits.
|
|
2136
|
+
for (const line of result.owners) {
|
|
2137
|
+
if (line.verdict === 'not-measured') {
|
|
2138
|
+
console.log();
|
|
2139
|
+
console.log(` ${c.yellow('!')} ${wrap(t.owners.notMeasured(line.owner), 74, ' ')}`);
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
console.log();
|
|
2143
|
+
if (result.unallocated.usd > 0) {
|
|
2144
|
+
console.log(` ${c.yellow('!')} ${wrap(t.owners.unallocated(formatUsd(result.unallocated.usd), report.total.totalUsd > 0 ? pct(result.unallocated.usd / report.total.totalUsd) : '—', result.unallocated.labels.slice(0, 6).join(', ')), 74, ' ')}`);
|
|
2145
|
+
console.log(` ${c.dim(wrap(t.owners.neverSpread(), 72, ' '))}`);
|
|
2146
|
+
}
|
|
2147
|
+
else {
|
|
2148
|
+
console.log(` ${c.dim(t.owners.nothingUnallocated())}`);
|
|
2149
|
+
}
|
|
2150
|
+
/**
|
|
2151
|
+
* The shared rules, printed with the report.
|
|
2152
|
+
*
|
|
2153
|
+
* The whole design: the argument then happens about the rule — "why is
|
|
2154
|
+
* search 60/40?" — rather than about the number, which is an argument nobody
|
|
2155
|
+
* can win because nobody can see where the number came from.
|
|
2156
|
+
*/
|
|
2157
|
+
if (result.sharedApplied.length > 0) {
|
|
2158
|
+
console.log();
|
|
2159
|
+
console.log(` ${c.bold(t.owners.sharedHeading())}`);
|
|
2160
|
+
for (const { label, split } of result.sharedApplied) {
|
|
2161
|
+
const rule = Object.entries(split)
|
|
2162
|
+
.map(([owner, share]) => `${owner} ${pct(share)}`)
|
|
2163
|
+
.join(', ');
|
|
2164
|
+
console.log(` ${c.dim(t.owners.sharedRule(label, rule))}`);
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
console.log();
|
|
2168
|
+
}
|
|
1969
2169
|
function commandModels(t, pricing) {
|
|
1970
2170
|
const n = (value) => value.toLocaleString(t.numberLocale);
|
|
1971
2171
|
const col = t.models.columns;
|
|
@@ -7013,6 +7213,12 @@ async function main() {
|
|
|
7013
7213
|
case 'models':
|
|
7014
7214
|
commandModels(t, pricing);
|
|
7015
7215
|
break;
|
|
7216
|
+
case 'owners':
|
|
7217
|
+
await commandOwners(args, config, pricing, t);
|
|
7218
|
+
break;
|
|
7219
|
+
case 'semantic':
|
|
7220
|
+
await commandSemantic(args, config, pricing, t);
|
|
7221
|
+
break;
|
|
7016
7222
|
case 'quality':
|
|
7017
7223
|
await commandQuality(args, config, pricing, t);
|
|
7018
7224
|
break;
|