@trazum/cli 1.50.8 → 1.50.9
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 +34 -0
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/es.d.ts.map +1 -1
- package/dist/i18n/es.js +34 -0
- package/dist/i18n/es.js.map +1 -1
- package/dist/i18n/types.d.ts +14 -0
- package/dist/i18n/types.d.ts.map +1 -1
- package/dist/index.js +94 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/i18n/en.ts +42 -0
- package/src/i18n/es.ts +42 -0
- package/src/i18n/types.ts +15 -0
- package/src/index.ts +128 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trazum/cli",
|
|
3
|
-
"version": "1.50.
|
|
3
|
+
"version": "1.50.9",
|
|
4
4
|
"description": "Trazum CLI: find where your LLM bill goes, price every finding per month, and enforce token budgets in CI.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "David Mu\u00f1oz Rey",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"prepublishOnly": "npm run build && npm test"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@trazum/core": "1.50.
|
|
40
|
+
"@trazum/core": "1.50.9"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@types/node": "^26.2.0",
|
package/src/i18n/en.ts
CHANGED
|
@@ -60,6 +60,7 @@ ${bold('USAGE')}
|
|
|
60
60
|
trazum gateway <anthropic|openai> --on-cannot-tell <fail-open|fail-closed>
|
|
61
61
|
trazum experiment <log> --a <label> --b <label> --min-outcomes <n>
|
|
62
62
|
trazum quality <log> --label <name> --at <iso> [--gate]
|
|
63
|
+
trazum semantic <prompt> [--yes]
|
|
63
64
|
trazum ladder <log>
|
|
64
65
|
trazum feedback
|
|
65
66
|
trazum --version
|
|
@@ -147,6 +148,17 @@ ${bold('OPTIONS FOR quality')}
|
|
|
147
148
|
whenever the model mix, the call volume or the outcome coverage moved across
|
|
148
149
|
the boundary — the prompt is not the only variable and it says so.
|
|
149
150
|
|
|
151
|
+
${bold('OPTIONS FOR semantic')}
|
|
152
|
+
--yes Required to send anything. Without it the command
|
|
153
|
+
prints what the call would cost and stops.
|
|
154
|
+
--model <id> Which model to ask. Priced from the catalogue.
|
|
155
|
+
|
|
156
|
+
Finds what a dictionary cannot: the same rule taught twice in different
|
|
157
|
+
words, an instruction restated far away, a policy a later clause contradicts.
|
|
158
|
+
Every quoted passage is checked character for character against the prompt,
|
|
159
|
+
pairs the rules engine already catches are dropped, and every token figure is
|
|
160
|
+
counted rather than believed. Optional, and always will be.
|
|
161
|
+
|
|
150
162
|
${bold('OPTIONS FOR prune')}
|
|
151
163
|
--cases <file> One input per line, or a JSON array. Required.
|
|
152
164
|
--yes Actually spend the calls. Without it the estimate is
|
|
@@ -992,6 +1004,36 @@ ${bold('EXAMPLES')}
|
|
|
992
1004
|
`${path} exists and could not be parsed, so nothing was written over it. Fix or move it first.`,
|
|
993
1005
|
},
|
|
994
1006
|
|
|
1007
|
+
semantic: {
|
|
1008
|
+
heading: (path) => `Semantic pass on ${path}`,
|
|
1009
|
+
willCost: (usd, input, output, model) =>
|
|
1010
|
+
`This will send the prompt to ${model}: about ${input} tokens in and ${output} out, roughly ${usd}. Estimated, not measured \u2014 a tool that spends your money to tell you how to spend less should be the first thing audited by its own arithmetic. Pass --yes to run it.`,
|
|
1011
|
+
needsYes: () => 'Nothing was sent. Add --yes once you have read the price above.',
|
|
1012
|
+
finding: (kind, because) =>
|
|
1013
|
+
`${kind === 'contradiction' ? 'Contradiction' : kind === 'restated-instruction' ? 'Restated' : 'Same thing, different words'}: ${because}`,
|
|
1014
|
+
span: (line, text) => `line ${line}: ${text}`,
|
|
1015
|
+
ceiling: (tokens) =>
|
|
1016
|
+
`At most ${tokens} tokens \u2014 a ceiling, not a saving. Merging these two means writing one passage that does the work of both, and nobody knows yet how long that is.`,
|
|
1017
|
+
noCeiling: () =>
|
|
1018
|
+
'No tokens attached. A contradiction is worth fixing because the prompt is wrong, not because it is long, and putting a figure on it would sell the wrong reason.',
|
|
1019
|
+
nothingFound: () => 'Nothing survived checking. That is a real answer.',
|
|
1020
|
+
rejected: (count) => `${count} proposals did not survive checking against the prompt.`,
|
|
1021
|
+
rejectedLine: (reason, span) =>
|
|
1022
|
+
reason === 'span-not-found'
|
|
1023
|
+
? `paraphrased its own evidence: ${span}`
|
|
1024
|
+
: reason === 'already-detected'
|
|
1025
|
+
? `already found without a model: ${span}`
|
|
1026
|
+
: reason === 'contradiction-of-a-copy'
|
|
1027
|
+
? `called a near-copy a contradiction: ${span}`
|
|
1028
|
+
: reason === 'spans-identical' || reason === 'spans-overlap'
|
|
1029
|
+
? `quoted the same passage twice: ${span}`
|
|
1030
|
+
: `covers ground an accepted finding covers: ${span}`,
|
|
1031
|
+
disposes: () =>
|
|
1032
|
+
'The model proposes and the deterministic layer disposes: every quoted passage is checked character for character against the prompt, pairs the rules engine already catches are dropped, and every token figure is counted here rather than believed.',
|
|
1033
|
+
optIn: () =>
|
|
1034
|
+
'This pass is optional and always will be. Trazum works with no key, no network and no model \u2014 that has been true since 0.1.0 and this does not change it.',
|
|
1035
|
+
},
|
|
1036
|
+
|
|
995
1037
|
quality: {
|
|
996
1038
|
heading: (label) => `Quality across the change: ${label}`,
|
|
997
1039
|
needsLabel: () => 'Name the workload with --label: this compares one label before and after a change, and a mixture of workloads would average a regression away.',
|
package/src/i18n/es.ts
CHANGED
|
@@ -47,6 +47,7 @@ ${bold('USO')}
|
|
|
47
47
|
trazum gateway <anthropic|openai> --on-cannot-tell <fail-open|fail-closed>
|
|
48
48
|
trazum experiment <log> --a <label> --b <label> --min-outcomes <n>
|
|
49
49
|
trazum quality <log> --label <name> --at <iso> [--gate]
|
|
50
|
+
trazum semantic <prompt> [--yes]
|
|
50
51
|
trazum ladder <log>
|
|
51
52
|
trazum feedback
|
|
52
53
|
trazum --version
|
|
@@ -137,6 +138,17 @@ ${bold('OPCIONES DE quality')}
|
|
|
137
138
|
siempre que la mezcla de modelos, el volumen o la cobertura se movieran a
|
|
138
139
|
través de la frontera — el prompt no es la única variable y lo dice.
|
|
139
140
|
|
|
141
|
+
${bold('OPCIONES DE semantic')}
|
|
142
|
+
--yes Obligatorio para enviar nada. Sin él, el comando
|
|
143
|
+
imprime lo que costaría la llamada y para.
|
|
144
|
+
--model <id> Qué modelo preguntar. Se tarifa del catálogo.
|
|
145
|
+
|
|
146
|
+
Encuentra lo que un diccionario no puede: la misma regla enseñada dos veces
|
|
147
|
+
con otras palabras, una instrucción repetida lejos, una política que una
|
|
148
|
+
cláusula posterior contradice. Cada pasaje citado se comprueba carácter a
|
|
149
|
+
carácter contra el prompt, los pares que el motor de reglas ya caza se
|
|
150
|
+
descartan, y toda cifra de tokens se cuenta en vez de creérsela.
|
|
151
|
+
|
|
140
152
|
${bold('OPCIONES DE prune')}
|
|
141
153
|
--cases <fichero> Una entrada por línea, o un array JSON. Obligatorio.
|
|
142
154
|
--yes Gasta las llamadas de verdad. Sin él se imprime la
|
|
@@ -1028,6 +1040,36 @@ ${bold('EJEMPLOS')}
|
|
|
1028
1040
|
`${path} existe y no se pudo interpretar, así que no se escribió nada encima. Arréglalo o muévelo primero.`,
|
|
1029
1041
|
},
|
|
1030
1042
|
|
|
1043
|
+
semantic: {
|
|
1044
|
+
heading: (path) => `Pase sem\u00e1ntico sobre ${path}`,
|
|
1045
|
+
willCost: (usd, input, output, model) =>
|
|
1046
|
+
`Esto enviar\u00e1 el prompt a ${model}: unos ${input} tokens de entrada y ${output} de salida, aproximadamente ${usd}. Estimado, no medido \u2014 una herramienta que gasta tu dinero para decirte c\u00f3mo gastar menos deber\u00eda ser lo primero que audite su propia aritm\u00e9tica. Pasa --yes para ejecutarlo.`,
|
|
1047
|
+
needsYes: () => 'No se envi\u00f3 nada. A\u00f1ade --yes cuando hayas le\u00eddo el precio de arriba.',
|
|
1048
|
+
finding: (kind, because) =>
|
|
1049
|
+
`${kind === 'contradiction' ? 'Contradicci\u00f3n' : kind === 'restated-instruction' ? 'Repetido' : 'Lo mismo con otras palabras'}: ${because}`,
|
|
1050
|
+
span: (line, text) => `l\u00ednea ${line}: ${text}`,
|
|
1051
|
+
ceiling: (tokens) =>
|
|
1052
|
+
`Como mucho ${tokens} tokens \u2014 un techo, no un ahorro. Fundir los dos significa escribir un pasaje que haga el trabajo de ambos, y nadie sabe todav\u00eda cu\u00e1nto ocupa.`,
|
|
1053
|
+
noCeiling: () =>
|
|
1054
|
+
'Sin tokens asociados. Una contradicci\u00f3n merece arreglarse porque el prompt est\u00e1 mal, no porque sea largo, y ponerle una cifra vender\u00eda el motivo equivocado.',
|
|
1055
|
+
nothingFound: () => 'Nada sobrevivi\u00f3 a la comprobaci\u00f3n. Esa es una respuesta real.',
|
|
1056
|
+
rejected: (count) => `${count} propuestas no sobrevivieron a la comprobaci\u00f3n contra el prompt.`,
|
|
1057
|
+
rejectedLine: (reason, span) =>
|
|
1058
|
+
reason === 'span-not-found'
|
|
1059
|
+
? `parafrase\u00f3 su propia evidencia: ${span}`
|
|
1060
|
+
: reason === 'already-detected'
|
|
1061
|
+
? `ya se encuentra sin modelo: ${span}`
|
|
1062
|
+
: reason === 'contradiction-of-a-copy'
|
|
1063
|
+
? `llam\u00f3 contradicci\u00f3n a una casi copia: ${span}`
|
|
1064
|
+
: reason === 'spans-identical' || reason === 'spans-overlap'
|
|
1065
|
+
? `cit\u00f3 el mismo pasaje dos veces: ${span}`
|
|
1066
|
+
: `cubre terreno que ya cubre un hallazgo aceptado: ${span}`,
|
|
1067
|
+
disposes: () =>
|
|
1068
|
+
'El modelo propone y la capa determinista dispone: cada pasaje citado se comprueba car\u00e1cter a car\u00e1cter contra el prompt, los pares que el motor de reglas ya caza se descartan, y toda cifra de tokens se cuenta aqu\u00ed en vez de cre\u00e9rsela.',
|
|
1069
|
+
optIn: () =>
|
|
1070
|
+
'Este pase es opcional y siempre lo ser\u00e1. Trazum funciona sin clave, sin red y sin modelo \u2014 eso es cierto desde 0.1.0 y esto no lo cambia.',
|
|
1071
|
+
},
|
|
1072
|
+
|
|
1031
1073
|
quality: {
|
|
1032
1074
|
heading: (label) => `Calidad a trav\u00e9s del cambio: ${label}`,
|
|
1033
1075
|
needsLabel: () => 'Nombra la carga con --label: esto compara una etiqueta antes y despu\u00e9s de un cambio, y una mezcla de cargas promediar\u00eda una regresi\u00f3n hasta hacerla desaparecer.',
|
package/src/i18n/types.ts
CHANGED
|
@@ -268,6 +268,21 @@ export interface CliMessages {
|
|
|
268
268
|
* The ladder. Every line here exists to stop somebody reading "we route to
|
|
269
269
|
* the cheap model first" as a saving without the number that decides it.
|
|
270
270
|
*/
|
|
271
|
+
semantic: {
|
|
272
|
+
heading(path: string): string;
|
|
273
|
+
willCost(usd: string, input: string, output: string, model: string): string;
|
|
274
|
+
needsYes(): string;
|
|
275
|
+
finding(kind: string, because: string): string;
|
|
276
|
+
span(line: string, text: string): string;
|
|
277
|
+
ceiling(tokens: string): string;
|
|
278
|
+
noCeiling(): string;
|
|
279
|
+
nothingFound(): string;
|
|
280
|
+
rejected(count: string): string;
|
|
281
|
+
rejectedLine(reason: string, span: string): string;
|
|
282
|
+
disposes(): string;
|
|
283
|
+
optIn(): string;
|
|
284
|
+
};
|
|
285
|
+
|
|
271
286
|
quality: {
|
|
272
287
|
heading(label: string): string;
|
|
273
288
|
needsLabel(): string;
|
package/src/index.ts
CHANGED
|
@@ -42,6 +42,9 @@ import {
|
|
|
42
42
|
BREAK_EVEN_BAND,
|
|
43
43
|
runExperiment,
|
|
44
44
|
qualityGate,
|
|
45
|
+
semanticPassCost,
|
|
46
|
+
verifySemanticProposals,
|
|
47
|
+
SEMANTIC_SYSTEM_PROMPT,
|
|
45
48
|
ladderPosition,
|
|
46
49
|
validateLadder,
|
|
47
50
|
outcomeReport,
|
|
@@ -143,6 +146,7 @@ import type {
|
|
|
143
146
|
ContractName,
|
|
144
147
|
ExperimentArm,
|
|
145
148
|
GateSide,
|
|
149
|
+
SemanticProposal,
|
|
146
150
|
FailurePolicy,
|
|
147
151
|
GatewayStanding,
|
|
148
152
|
UsageProfileReport,
|
|
@@ -584,6 +588,7 @@ const COMMAND_FLAGS: Record<string, string[]> = {
|
|
|
584
588
|
ladder: ['pricing', 'pricing-live', 'since', 'until', 'label'],
|
|
585
589
|
experiment: ['a', 'b', 'min-outcomes', 'pricing', 'pricing-live'],
|
|
586
590
|
quality: ['label', 'at', 'gate', 'pricing', 'pricing-live'],
|
|
591
|
+
semantic: ['yes', 'model', 'pricing', 'pricing-live'],
|
|
587
592
|
where: [],
|
|
588
593
|
rules: [],
|
|
589
594
|
blame: ['limit', 'model', 'calls', 'output-tokens', 'batch', 'prompt', 'markdown-out'],
|
|
@@ -2609,6 +2614,126 @@ async function commandQuality(
|
|
|
2609
2614
|
console.log();
|
|
2610
2615
|
}
|
|
2611
2616
|
|
|
2617
|
+
/**
|
|
2618
|
+
* `trazum semantic <prompt> [--yes]` — the findings a dictionary cannot see.
|
|
2619
|
+
*
|
|
2620
|
+
* The rules engine has deferred these since 0.1.0 for one honest reason: a
|
|
2621
|
+
* dictionary cannot see meaning, and a model that hallucinates a finding is
|
|
2622
|
+
* worse than a rule that misses one.
|
|
2623
|
+
*
|
|
2624
|
+
* **The price is printed before anything is sent, and `--yes` is required.** A
|
|
2625
|
+
* tool that spends somebody's money to tell them how to spend less has to be
|
|
2626
|
+
* the first thing audited by its own arithmetic, and it has to ask.
|
|
2627
|
+
*/
|
|
2628
|
+
async function commandSemantic(
|
|
2629
|
+
args: Args,
|
|
2630
|
+
config: TrazumConfig,
|
|
2631
|
+
pricing: PricingCatalogue,
|
|
2632
|
+
t: CliMessages,
|
|
2633
|
+
): Promise<void> {
|
|
2634
|
+
const prompt = await readInput(args.positional[0], t);
|
|
2635
|
+
const modelId = stringFlag(args, 'model') ?? config.usage?.model ?? DEFAULT_USAGE.model;
|
|
2636
|
+
const model = pricing.byId.get(modelId) ?? getModel(DEFAULT_USAGE.model);
|
|
2637
|
+
const rates = { inputPerMTok: model.inputPerMTok, outputPerMTok: model.outputPerMTok };
|
|
2638
|
+
const cost = semanticPassCost(prompt, rates);
|
|
2639
|
+
const n = (value: number): string => value.toLocaleString(t.numberLocale);
|
|
2640
|
+
|
|
2641
|
+
console.log();
|
|
2642
|
+
console.log(c.bold(t.semantic.heading(args.positional[0] ?? '-')));
|
|
2643
|
+
console.log();
|
|
2644
|
+
console.log(
|
|
2645
|
+
` ${wrap(
|
|
2646
|
+
t.semantic.willCost(formatUsd(cost.usd), n(cost.inputTokens), n(cost.outputTokens), model.displayName),
|
|
2647
|
+
74,
|
|
2648
|
+
' ',
|
|
2649
|
+
)}`,
|
|
2650
|
+
);
|
|
2651
|
+
|
|
2652
|
+
if (!boolFlag(args, 'yes')) {
|
|
2653
|
+
// Nothing has been sent at this point, and nothing will be. The price
|
|
2654
|
+
// above is the whole output of a run without --yes.
|
|
2655
|
+
console.log();
|
|
2656
|
+
console.log(` ${c.dim(t.semantic.needsYes())}`);
|
|
2657
|
+
console.log();
|
|
2658
|
+
return;
|
|
2659
|
+
}
|
|
2660
|
+
|
|
2661
|
+
const provider = providerFromEnv();
|
|
2662
|
+
if (!provider) throw new Error(t.errors.llmNotConfigured());
|
|
2663
|
+
|
|
2664
|
+
const answer = await provider.complete({ system: SEMANTIC_SYSTEM_PROMPT, user: prompt });
|
|
2665
|
+
let proposals: SemanticProposal[] = [];
|
|
2666
|
+
try {
|
|
2667
|
+
const parsed: unknown = JSON.parse(
|
|
2668
|
+
/^(?:```|~~~)[a-zA-Z]*\n([\s\S]*?)\n?(?:```|~~~)$/.exec(answer.trim())?.[1] ?? answer.trim(),
|
|
2669
|
+
);
|
|
2670
|
+
/**
|
|
2671
|
+
* A response that is not the shape asked for is **no proposals**, never a
|
|
2672
|
+
* crash and never a partial read. The model was told exactly what to
|
|
2673
|
+
* return; anything else is a response this layer cannot check, and an
|
|
2674
|
+
* unchecked finding is the one thing this whole module exists to prevent.
|
|
2675
|
+
*/
|
|
2676
|
+
if (Array.isArray(parsed)) {
|
|
2677
|
+
proposals = parsed.filter(
|
|
2678
|
+
(entry): entry is SemanticProposal =>
|
|
2679
|
+
typeof entry === 'object' &&
|
|
2680
|
+
entry !== null &&
|
|
2681
|
+
Array.isArray((entry as SemanticProposal).spans) &&
|
|
2682
|
+
(entry as SemanticProposal).spans.length === 2 &&
|
|
2683
|
+
(entry as SemanticProposal).spans.every((span) => typeof span === 'string'),
|
|
2684
|
+
);
|
|
2685
|
+
}
|
|
2686
|
+
} catch {
|
|
2687
|
+
proposals = [];
|
|
2688
|
+
}
|
|
2689
|
+
|
|
2690
|
+
const result = verifySemanticProposals(prompt, proposals);
|
|
2691
|
+
const lineOf = (offset: number): number => prompt.slice(0, offset).split('\n').length;
|
|
2692
|
+
|
|
2693
|
+
console.log();
|
|
2694
|
+
if (result.findings.length === 0) {
|
|
2695
|
+
console.log(` ${c.dim(t.semantic.nothingFound())}`);
|
|
2696
|
+
}
|
|
2697
|
+
for (const finding of result.findings) {
|
|
2698
|
+
console.log(` ${c.bold(t.semantic.finding(finding.kind, finding.because))}`);
|
|
2699
|
+
finding.spans.forEach((span, index) => {
|
|
2700
|
+
const shown = span.length > 90 ? `${span.slice(0, 87)}…` : span;
|
|
2701
|
+
console.log(` ${c.dim(t.semantic.span(String(lineOf(finding.offsets[index] ?? 0)), shown))}`);
|
|
2702
|
+
});
|
|
2703
|
+
console.log(
|
|
2704
|
+
` ${c.dim(
|
|
2705
|
+
wrap(
|
|
2706
|
+
finding.ceilingTokens > 0 ? t.semantic.ceiling(n(finding.ceilingTokens)) : t.semantic.noCeiling(),
|
|
2707
|
+
70,
|
|
2708
|
+
' ',
|
|
2709
|
+
),
|
|
2710
|
+
)}`,
|
|
2711
|
+
);
|
|
2712
|
+
console.log();
|
|
2713
|
+
}
|
|
2714
|
+
|
|
2715
|
+
/**
|
|
2716
|
+
* What did **not** survive, counted and reasoned.
|
|
2717
|
+
*
|
|
2718
|
+
* A pass that showed only its accepted findings would hide its own hit
|
|
2719
|
+
* rate, and the hit rate is the most useful thing a reader can know about
|
|
2720
|
+
* whether to run it again.
|
|
2721
|
+
*/
|
|
2722
|
+
if (result.rejected.length > 0) {
|
|
2723
|
+
console.log(` ${c.dim(t.semantic.rejected(n(result.rejected.length)))}`);
|
|
2724
|
+
for (const { proposal, reason } of result.rejected.slice(0, 5)) {
|
|
2725
|
+
const span = proposal.spans[0];
|
|
2726
|
+
const shown = span.length > 50 ? `${span.slice(0, 47)}…` : span;
|
|
2727
|
+
console.log(` ${c.dim(t.semantic.rejectedLine(reason, shown))}`);
|
|
2728
|
+
}
|
|
2729
|
+
console.log();
|
|
2730
|
+
}
|
|
2731
|
+
|
|
2732
|
+
console.log(` ${c.dim(wrap(t.semantic.disposes(), 74, ' '))}`);
|
|
2733
|
+
console.log(` ${c.dim(wrap(t.semantic.optIn(), 74, ' '))}`);
|
|
2734
|
+
console.log();
|
|
2735
|
+
}
|
|
2736
|
+
|
|
2612
2737
|
function commandModels(t: CliMessages, pricing: PricingCatalogue): void {
|
|
2613
2738
|
const n = (value: number): string => value.toLocaleString(t.numberLocale);
|
|
2614
2739
|
const col = t.models.columns;
|
|
@@ -8809,6 +8934,9 @@ async function main(): Promise<void> {
|
|
|
8809
8934
|
case 'models':
|
|
8810
8935
|
commandModels(t, pricing);
|
|
8811
8936
|
break;
|
|
8937
|
+
case 'semantic':
|
|
8938
|
+
await commandSemantic(args, config, pricing, t);
|
|
8939
|
+
break;
|
|
8812
8940
|
case 'quality':
|
|
8813
8941
|
await commandQuality(args, config, pricing, t);
|
|
8814
8942
|
break;
|