@trazum/cli 1.10.0 → 1.25.0
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/README.md +120 -2
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +249 -4
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/es.d.ts.map +1 -1
- package/dist/i18n/es.js +251 -4
- package/dist/i18n/es.js.map +1 -1
- package/dist/i18n/types.d.ts +499 -1
- package/dist/i18n/types.d.ts.map +1 -1
- package/dist/index.js +1586 -24
- package/dist/index.js.map +1 -1
- package/dist/markdown.d.ts +68 -0
- package/dist/markdown.d.ts.map +1 -1
- package/dist/markdown.js +329 -1
- package/dist/markdown.js.map +1 -1
- package/dist/time.d.ts +18 -0
- package/dist/time.d.ts.map +1 -0
- package/dist/time.js +32 -0
- package/dist/time.js.map +1 -0
- package/package.json +2 -2
- package/src/i18n/en.ts +370 -4
- package/src/i18n/es.ts +373 -4
- package/src/i18n/types.ts +522 -1
- package/src/index.ts +1857 -118
- package/src/markdown.ts +415 -1
- package/src/time.ts +32 -0
package/src/markdown.ts
CHANGED
|
@@ -8,7 +8,9 @@ import type {
|
|
|
8
8
|
PromptProfile,
|
|
9
9
|
RuleLevel,
|
|
10
10
|
} from '@trazum/core';
|
|
11
|
-
import { formatSignedUsd, formatUsd, getMessages, getModel } from '@trazum/core';
|
|
11
|
+
import { TTL_1H_MS, UNLABELLED, formatSignedUsd, formatUsd, getMessages, getModel, sharesOf } from '@trazum/core';
|
|
12
|
+
import { dayOf, formatGap, median, spanDays } from './time.js';
|
|
13
|
+
import type { AgainstDriver, BillLevers, CacheEconomics, RepriceReport, UsageProfileReport } from '@trazum/core';
|
|
12
14
|
|
|
13
15
|
/**
|
|
14
16
|
* Markdown for the places a pull request is actually read.
|
|
@@ -715,3 +717,415 @@ export function renderBlameMarkdown(input: BlameMarkdownInput): string {
|
|
|
715
717
|
|
|
716
718
|
return lines.join('\n');
|
|
717
719
|
}
|
|
720
|
+
|
|
721
|
+
export interface ProfileMarkdownInput {
|
|
722
|
+
report: UsageProfileReport;
|
|
723
|
+
levers: BillLevers;
|
|
724
|
+
cache: CacheEconomics;
|
|
725
|
+
t: CliMessages;
|
|
726
|
+
/**
|
|
727
|
+
* The `--since`/`--until` values as the user typed them, when a window was
|
|
728
|
+
* applied. Passed through rather than re-derived from `timeWindow`'s epoch
|
|
729
|
+
* bounds, because a bare `--until 2026-08-14` includes that whole day —
|
|
730
|
+
* rendering the internal exclusive bound would print the *next* day and
|
|
731
|
+
* disagree with the terminal about which window this was.
|
|
732
|
+
*/
|
|
733
|
+
window?: { since: string; until: string };
|
|
734
|
+
/**
|
|
735
|
+
* Passed only when the price table is old enough to matter, so the
|
|
736
|
+
* threshold lives once, beside the terminal's. Rendered loud: staleness
|
|
737
|
+
* does not name its own size the way a skipped line does.
|
|
738
|
+
*/
|
|
739
|
+
stalePricing?: { date: string; days: number };
|
|
740
|
+
/**
|
|
741
|
+
* The comparison, when `--against` was given — the section the terminal
|
|
742
|
+
* has had since 1.11 and the markdown did not, so a CI summary reporting
|
|
743
|
+
* on two logs showed only one of them. The drivers arrive computed (core's
|
|
744
|
+
* `driversBetween`) rather than derived here: the sign convention has one
|
|
745
|
+
* implementation, and this is a rendering.
|
|
746
|
+
*/
|
|
747
|
+
/**
|
|
748
|
+
* The repricing, when `--what-if` was given. Arrives computed from core's
|
|
749
|
+
* `repriceProfile` rather than derived here: three surfaces must not
|
|
750
|
+
* disagree about what a move would cost, and this is a rendering.
|
|
751
|
+
*/
|
|
752
|
+
whatIf?: RepriceReport | null;
|
|
753
|
+
against?: {
|
|
754
|
+
previousTotalUsd: number;
|
|
755
|
+
previousCalls: number;
|
|
756
|
+
labelDrivers: AgainstDriver[];
|
|
757
|
+
modelDrivers: AgainstDriver[];
|
|
758
|
+
/** True when both spans are known and intersect. */
|
|
759
|
+
overlap: { from: string; to: string } | null;
|
|
760
|
+
/** Nothing in the previous log could be priced — its own answer. */
|
|
761
|
+
nothingPriced: boolean;
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/**
|
|
766
|
+
* `trazum profile` as GitHub-flavoured markdown, for a job summary or a
|
|
767
|
+
* pull-request comment.
|
|
768
|
+
*
|
|
769
|
+
* The terminal report is the source of truth and this reuses its message
|
|
770
|
+
* catalogue line for line, because two renderings of the same finding drift the
|
|
771
|
+
* moment they are worded twice — the sign conventions here (`positive means
|
|
772
|
+
* worse` on the cache delta, ceilings that must be named as ceilings) have each
|
|
773
|
+
* already produced a bug when restated by hand.
|
|
774
|
+
*
|
|
775
|
+
* A finding that only exists in a terminal is a finding the reader's tooling
|
|
776
|
+
* never surfaces; this is the other half of the `--json` lesson, for humans
|
|
777
|
+
* reading CI instead of machines.
|
|
778
|
+
*/
|
|
779
|
+
export function renderProfileMarkdown(input: ProfileMarkdownInput): string {
|
|
780
|
+
const { report, levers, cache, t, window, stalePricing, against, whatIf } = input;
|
|
781
|
+
const n = (value: number): string => value.toLocaleString(t.numberLocale);
|
|
782
|
+
const pct = (share: number): string => `${(share * 100).toFixed(1)}%`;
|
|
783
|
+
const shares = sharesOf(report.total);
|
|
784
|
+
const showLabel = (label: string): string =>
|
|
785
|
+
label === UNLABELLED ? t.profile.unlabelled() : label;
|
|
786
|
+
|
|
787
|
+
const lines: string[] = [];
|
|
788
|
+
lines.push(`### ${t.profile.heading()}`);
|
|
789
|
+
lines.push('');
|
|
790
|
+
lines.push(`**${t.profile.spent(t.profile.calls(report.total.calls), formatUsd(report.total.totalUsd))}**`);
|
|
791
|
+
lines.push('');
|
|
792
|
+
// The span, under the same rule as the terminal: stated, never extrapolated,
|
|
793
|
+
// with partial coverage said in the same breath.
|
|
794
|
+
if (report.span !== null) {
|
|
795
|
+
const totalParsed = report.total.calls + report.unpriced.calls;
|
|
796
|
+
const partial =
|
|
797
|
+
report.span.calls < totalParsed
|
|
798
|
+
? ` ${mdText(t.profile.spanPartial(n(report.span.calls), n(totalParsed)))}`
|
|
799
|
+
: '';
|
|
800
|
+
lines.push(
|
|
801
|
+
`_${mdText(t.profile.spanLine(dayOf(report.span.fromMs), dayOf(report.span.toMs), spanDays(report.span.fromMs, report.span.toMs)))}${partial}_`,
|
|
802
|
+
);
|
|
803
|
+
lines.push('');
|
|
804
|
+
}
|
|
805
|
+
// The window before any figure, and the undated count loud — the same order
|
|
806
|
+
// and the same volume as the terminal, for the same reasons.
|
|
807
|
+
if (report.timeWindow !== null) {
|
|
808
|
+
lines.push(`_${mdText(t.profile.windowLine(window?.since ?? '—', window?.until ?? '—'))}_`);
|
|
809
|
+
lines.push('');
|
|
810
|
+
if (report.timeWindow.undatedExcluded > 0) {
|
|
811
|
+
lines.push(`> ⚠️ ${mdText(t.profile.windowUndated(report.timeWindow.undatedExcluded))}`);
|
|
812
|
+
lines.push('');
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
lines.push('| | USD | % | tokens |');
|
|
816
|
+
lines.push('|---|---:|---:|---:|');
|
|
817
|
+
const parts: Array<[string, number, number, number]> = [
|
|
818
|
+
[t.profile.partInput(), report.total.inputUsd, shares.input, report.total.inputTokens],
|
|
819
|
+
[t.profile.partCacheRead(), report.total.cacheReadUsd, shares.cacheRead, report.total.cacheReadTokens],
|
|
820
|
+
[t.profile.partCacheWrite(), report.total.cacheWriteUsd, shares.cacheWrite, report.total.cacheWriteTokens],
|
|
821
|
+
[t.profile.partOutput(), report.total.outputUsd, shares.output, report.total.outputTokens],
|
|
822
|
+
];
|
|
823
|
+
for (const [name, usd, share, tokens] of parts) {
|
|
824
|
+
// Catalogue text, not user data: no escaping into a code cell needed.
|
|
825
|
+
lines.push(`| ${name} | ${formatUsd(usd)} | ${pct(share)} | ${n(tokens)} |`);
|
|
826
|
+
}
|
|
827
|
+
lines.push('');
|
|
828
|
+
|
|
829
|
+
/**
|
|
830
|
+
* A doubled bill, said before anything above is believed. A CI summary
|
|
831
|
+
* showing a total nobody can trust is worse than one showing no total.
|
|
832
|
+
*/
|
|
833
|
+
if (report.duplicateLines.count > 0) {
|
|
834
|
+
lines.push(
|
|
835
|
+
`> ⚠️ ${mdText(t.profile.duplicateLines(report.duplicateLines.count, formatUsd(report.duplicateLines.usd)))}`,
|
|
836
|
+
);
|
|
837
|
+
lines.push('');
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
// The most expensive day against the median day, loud past twice it — the
|
|
841
|
+
// same sentence and the same yardstick the terminal prints.
|
|
842
|
+
if (report.spendByDay.length >= 2) {
|
|
843
|
+
const medianUsd = median(report.spendByDay.map((d) => d.usd));
|
|
844
|
+
const peak = report.spendByDay.reduce((a, b) => (b.usd > a.usd ? b : a));
|
|
845
|
+
if (medianUsd > 0) {
|
|
846
|
+
const sentence = t.profile.dayPeak(peak.day, formatUsd(peak.usd), (peak.usd / medianUsd).toFixed(1));
|
|
847
|
+
const labelClause =
|
|
848
|
+
peak.topLabel !== null && report.byLabel.length > 1
|
|
849
|
+
? ` ${t.profile.dayPeakLabel(showLabel(peak.topLabel), formatUsd(peak.topLabelUsd))}`
|
|
850
|
+
: '';
|
|
851
|
+
const loud = peak.usd > 2 * medianUsd;
|
|
852
|
+
lines.push(loud ? `> ⚠️ ${mdText(`${sentence}${labelClause}`)}` : `_${mdText(`${sentence}${labelClause}`)}_`);
|
|
853
|
+
lines.push('');
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
/**
|
|
857
|
+
* The series itself, most recent days last — the shape the peak sentence
|
|
858
|
+
* summarises, for the reader who wants to see the week. Capped at 14
|
|
859
|
+
* days with the earlier ones counted out loud: silent truncation reads
|
|
860
|
+
* as "covered everything" when it did not.
|
|
861
|
+
*/
|
|
862
|
+
const DAYS_SHOWN = 14;
|
|
863
|
+
const shown = report.spendByDay.slice(-DAYS_SHOWN);
|
|
864
|
+
lines.push(`| ${t.profile.dayTableDay()} | USD | ${t.profile.dayTableCalls()} | ${t.profile.dayTableTop()} |`);
|
|
865
|
+
lines.push('|---|---:|---:|---|');
|
|
866
|
+
for (const day of shown) {
|
|
867
|
+
const top = day.topLabel === null ? '—' : mdTextCell(showLabel(day.topLabel));
|
|
868
|
+
lines.push(`| ${day.day} | ${formatUsd(day.usd)} | ${n(day.calls)} | ${top} |`);
|
|
869
|
+
}
|
|
870
|
+
if (report.spendByDay.length > DAYS_SHOWN) {
|
|
871
|
+
lines.push('');
|
|
872
|
+
lines.push(`_${mdText(t.profile.dayTableEarlier(report.spendByDay.length - DAYS_SHOWN))}_`);
|
|
873
|
+
}
|
|
874
|
+
lines.push('');
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
lines.push(`#### ${t.profile.leversHeading()}`);
|
|
878
|
+
lines.push('');
|
|
879
|
+
if (levers.slices.length === 0) {
|
|
880
|
+
lines.push(mdText(t.profile.leversNone()));
|
|
881
|
+
} else {
|
|
882
|
+
for (const slice of levers.slices.slice(0, 5)) {
|
|
883
|
+
lines.push(
|
|
884
|
+
`- **${mdText(t.profile.leverSlice(showLabel(slice.label), slice.modelName, formatUsd(slice.combinedUsd), pct(slice.shareOfBill)))}** — ${mdText(t.profile.leverCalls(t.profile.calls(slice.calls), formatUsd(slice.spentUsd)))}`,
|
|
885
|
+
);
|
|
886
|
+
if (slice.route) {
|
|
887
|
+
lines.push(` - ${mdText(t.profile.leverRoute(slice.route.candidate.displayName, formatUsd(slice.route.savingUsd)))}`);
|
|
888
|
+
}
|
|
889
|
+
if (slice.batch) {
|
|
890
|
+
lines.push(` - ${mdText(t.profile.leverBatch(formatUsd(slice.batch.savingUsd)))}`);
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
lines.push('');
|
|
895
|
+
lines.push(`_${mdText(t.profile.leverPromptCeiling(formatUsd(levers.promptCeilingUsd), pct(levers.promptCeilingShare)))}_`);
|
|
896
|
+
lines.push('');
|
|
897
|
+
|
|
898
|
+
/**
|
|
899
|
+
* The same request sent again — money that bought nothing the call before
|
|
900
|
+
* it had not already paid for. Loud in a summary, and hedged in the same
|
|
901
|
+
* words the terminal uses.
|
|
902
|
+
*/
|
|
903
|
+
if (report.repeatedTurns.length > 0) {
|
|
904
|
+
lines.push(`#### ${t.profile.repeatsHeading()}`);
|
|
905
|
+
lines.push('');
|
|
906
|
+
for (const row of report.repeatedTurns.slice(0, 3)) {
|
|
907
|
+
lines.push(
|
|
908
|
+
`> ⚠️ ${mdText(t.profile.repeatsFound(showLabel(row.label), row.modelName, n(row.repeats), n(row.checkedCalls), n(Math.round(row.withinMs / 1000)), formatUsd(row.usd)))}`,
|
|
909
|
+
);
|
|
910
|
+
lines.push('');
|
|
911
|
+
}
|
|
912
|
+
lines.push(`_${mdText(t.profile.repeatsAdvice())}_`);
|
|
913
|
+
lines.push('');
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
/**
|
|
917
|
+
* How big the calls are — the half of the bill the totals table can only
|
|
918
|
+
* name. Same threshold and same two sentences as the terminal, because a
|
|
919
|
+
* CI summary that summarises differently is a second opinion nobody asked
|
|
920
|
+
* for.
|
|
921
|
+
*/
|
|
922
|
+
if (report.inputShapes.length > 0) {
|
|
923
|
+
lines.push(`#### ${t.profile.inputShapeHeading()}`);
|
|
924
|
+
lines.push('');
|
|
925
|
+
for (const shape of report.inputShapes.slice(0, 3)) {
|
|
926
|
+
const who = showLabel(shape.label);
|
|
927
|
+
if (shape.medianWithinTokens === null || shape.p95WithinTokens === null || shape.p95OverMedian === null) {
|
|
928
|
+
lines.push(`- ${mdText(t.profile.inputHuge(who, shape.modelName, t.profile.calls(shape.calls), formatUsd(shape.inputUsd)))}`);
|
|
929
|
+
continue;
|
|
930
|
+
}
|
|
931
|
+
const skewed = shape.p95OverMedian >= 4;
|
|
932
|
+
lines.push(
|
|
933
|
+
`- **${mdText(skewed
|
|
934
|
+
? t.profile.inputSkewed(who, shape.modelName, n(shape.medianWithinTokens), n(shape.p95WithinTokens), shape.p95OverMedian.toFixed(1), formatUsd(shape.inputUsd))
|
|
935
|
+
: t.profile.inputEven(who, shape.modelName, n(shape.medianWithinTokens), n(shape.p95WithinTokens), formatUsd(shape.inputUsd)))}**`,
|
|
936
|
+
);
|
|
937
|
+
lines.push(` - ${mdText(skewed ? t.profile.inputSkewedAdvice() : t.profile.inputEvenAdvice())}`);
|
|
938
|
+
if (shape.cachedShare >= 0.5) {
|
|
939
|
+
lines.push(` - ${mdText(t.profile.inputMostlyCached(pct(shape.cachedShare)))}`);
|
|
940
|
+
} else if (shape.cachedShare < 0.1) {
|
|
941
|
+
lines.push(` - ${mdText(t.profile.inputFullRate())}`);
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
lines.push('');
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
/**
|
|
948
|
+
* The repricing, with the assumption above the figure here too — a pull
|
|
949
|
+
* request comment is exactly where a dollar amount with the caveat
|
|
950
|
+
* underneath would be read as a recommendation and merged.
|
|
951
|
+
*/
|
|
952
|
+
if (whatIf !== undefined && whatIf !== null) {
|
|
953
|
+
lines.push(`#### ${t.profile.whatIfHeading(whatIf.target.displayName)}`);
|
|
954
|
+
lines.push('');
|
|
955
|
+
lines.push(`_${mdText(t.profile.whatIfAssumption())}_`);
|
|
956
|
+
lines.push('');
|
|
957
|
+
if (whatIf.slices.length === 0) {
|
|
958
|
+
lines.push(mdText(t.profile.whatIfNothingToMove()));
|
|
959
|
+
lines.push('');
|
|
960
|
+
} else {
|
|
961
|
+
lines.push(
|
|
962
|
+
`**${mdText(t.profile.whatIfTotal(formatUsd(whatIf.currentUsd), formatUsd(whatIf.targetUsd), formatUsd(Math.abs(whatIf.deltaUsd))))}**`,
|
|
963
|
+
);
|
|
964
|
+
lines.push('');
|
|
965
|
+
for (const slice of whatIf.slices.slice(0, 5)) {
|
|
966
|
+
lines.push(`- ${mdText(t.profile.whatIfSlice(showLabel(slice.label), slice.model, formatUsd(slice.currentUsd), formatUsd(slice.targetUsd)))}`);
|
|
967
|
+
}
|
|
968
|
+
lines.push('');
|
|
969
|
+
}
|
|
970
|
+
for (const slice of whatIf.overContext.slice(0, 3)) {
|
|
971
|
+
lines.push(
|
|
972
|
+
`> ⚠️ ${mdText(t.profile.whatIfOverContext(showLabel(slice.label), n(slice.maxCallInputTokens), n(whatIf.target.contextWindow), formatUsd(slice.currentUsd)))}`,
|
|
973
|
+
);
|
|
974
|
+
lines.push('');
|
|
975
|
+
}
|
|
976
|
+
if (whatIf.alreadyOnTarget.calls > 0) {
|
|
977
|
+
lines.push(`_${mdText(t.profile.whatIfAlreadyThere(t.profile.calls(whatIf.alreadyOnTarget.calls), formatUsd(whatIf.alreadyOnTarget.usd)))}_`);
|
|
978
|
+
lines.push('');
|
|
979
|
+
}
|
|
980
|
+
if (whatIf.unpricedCalls > 0) {
|
|
981
|
+
lines.push(`_${mdText(t.profile.whatIfUnpriced(t.profile.calls(whatIf.unpricedCalls), whatIf.unpricedModels.join(', ')))}_`);
|
|
982
|
+
lines.push('');
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
// The cache verdict, with the same refusal to answer an unsettled question.
|
|
987
|
+
const unsettled = cache.worstCaseVerdict !== cache.verdict && report.total.assumedWriteTtlCalls > 0;
|
|
988
|
+
if (unsettled) {
|
|
989
|
+
lines.push(`> ⚠️ ${mdText(t.profile.cacheTtlUnsettled(report.total.assumedWriteTtlCalls, formatUsd(-cache.deltaUsd), formatUsd(cache.worstCaseDeltaUsd)))}`);
|
|
990
|
+
lines.push('');
|
|
991
|
+
} else if (cache.verdict === 'lost-money') {
|
|
992
|
+
lines.push(`> ⚠️ ${mdText(t.profile.cacheLost(formatUsd(cache.deltaUsd), n(report.total.cacheWriteTokens), n(report.total.cacheReadTokens)))}`);
|
|
993
|
+
lines.push('');
|
|
994
|
+
} else if (cache.verdict === 'paid-off') {
|
|
995
|
+
lines.push(mdText(t.profile.cachePaidOff(formatUsd(-cache.deltaUsd))));
|
|
996
|
+
lines.push('');
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
// Whether the TTL fits the gaps — the mechanism behind the verdict above,
|
|
1000
|
+
// with the two failing verdicts loud and the rest quiet, as on the terminal.
|
|
1001
|
+
for (const fit of report.cacheTtlFit.slice(0, 3)) {
|
|
1002
|
+
const who = showLabel(fit.label);
|
|
1003
|
+
const gap = formatGap(fit.medianGapMs);
|
|
1004
|
+
const sentence =
|
|
1005
|
+
fit.verdict === 'expires-before-reuse'
|
|
1006
|
+
? fit.medianGapMs > TTL_1H_MS
|
|
1007
|
+
? t.profile.ttlFitExpiresBoth(who, fit.modelName, gap)
|
|
1008
|
+
: t.profile.ttlFitExpires(who, fit.modelName, gap)
|
|
1009
|
+
: fit.verdict === 'overlong-ttl'
|
|
1010
|
+
? t.profile.ttlFitOverlong(who, fit.modelName, gap, formatUsd(fit.overpayUsd))
|
|
1011
|
+
: fit.verdict === 'unsettled'
|
|
1012
|
+
? t.profile.ttlFitUnsettledGap(who, fit.modelName, gap)
|
|
1013
|
+
: t.profile.ttlFitFits(who, fit.modelName, gap);
|
|
1014
|
+
const loud = fit.verdict === 'expires-before-reuse' || fit.verdict === 'overlong-ttl';
|
|
1015
|
+
lines.push(loud ? `> ⚠️ ${mdText(sentence)}` : `_${mdText(sentence)}_`);
|
|
1016
|
+
lines.push('');
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
// Conversations that never came back: the fact loud, the ceiling quiet —
|
|
1020
|
+
// decided by the slice's own reads, exactly as on the terminal.
|
|
1021
|
+
const readsBySlice = new Map(
|
|
1022
|
+
report.byLabelAndModel.map((r) => [`${r.label}\n${r.model}`, r.breakdown.cacheReadTokens]),
|
|
1023
|
+
);
|
|
1024
|
+
for (const row of report.singleTurnCacheWrites.slice(0, 3)) {
|
|
1025
|
+
const who = showLabel(row.label);
|
|
1026
|
+
const reads = readsBySlice.get(`${row.label}\n${row.model}`) ?? 0;
|
|
1027
|
+
const sentence =
|
|
1028
|
+
reads === 0
|
|
1029
|
+
? t.profile.singleTurnConfirmed(who, row.modelName, n(row.singleTurnSessions), n(row.sessions), formatUsd(row.singleTurnWriteUsd))
|
|
1030
|
+
: t.profile.singleTurnCeiling(who, row.modelName, n(row.singleTurnSessions), n(row.sessions), formatUsd(row.singleTurnWriteUsd));
|
|
1031
|
+
lines.push(reads === 0 ? `> ⚠️ ${mdText(sentence)}` : `_${mdText(sentence)}_`);
|
|
1032
|
+
lines.push('');
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
for (const growth of report.conversations.slice(0, 3)) {
|
|
1036
|
+
lines.push(
|
|
1037
|
+
`- ${mdText(t.profile.historyGrowth(showLabel(growth.label), growth.modelName, n(Math.round(growth.minTurnTokens)), n(Math.round(growth.maxTurnTokens)), n(growth.longestSession)))} ${mdText(t.profile.historyCeiling(formatUsd(growth.growthUsd), pct(growth.shareOfBill), formatUsd(growth.flatUsd), formatUsd(growth.inputUsd)))}`,
|
|
1038
|
+
);
|
|
1039
|
+
}
|
|
1040
|
+
if (report.conversations.length > 0) lines.push('');
|
|
1041
|
+
|
|
1042
|
+
if (report.total.truncatedCalls > 0 && report.total.outputUsd > 0) {
|
|
1043
|
+
lines.push(
|
|
1044
|
+
`> ⚠️ ${mdText(t.profile.truncatedWaste(t.profile.calls(report.total.truncatedCalls), formatUsd(report.total.truncatedOutputUsd), pct(report.total.truncatedOutputUsd / report.total.outputUsd)))}`,
|
|
1045
|
+
);
|
|
1046
|
+
lines.push('');
|
|
1047
|
+
// The suspects, with the rate over calls that measured — the terminal's
|
|
1048
|
+
// denominator, because a workload logging the field half the time is not
|
|
1049
|
+
// one whose other half completed.
|
|
1050
|
+
const truncatedLabels = report.byLabel
|
|
1051
|
+
.filter((row) => row.breakdown.truncatedCalls > 0)
|
|
1052
|
+
.sort((a, b) => b.breakdown.truncatedOutputUsd - a.breakdown.truncatedOutputUsd);
|
|
1053
|
+
if (truncatedLabels.length > 0 && report.byLabel.length > 1) {
|
|
1054
|
+
for (const row of truncatedLabels.slice(0, 3)) {
|
|
1055
|
+
lines.push(
|
|
1056
|
+
`- ${mdText(t.profile.truncatedBy(showLabel(row.label), n(row.breakdown.truncatedCalls), n(row.breakdown.stopReasonCalls), pct(row.breakdown.truncatedCalls / row.breakdown.stopReasonCalls), formatUsd(row.breakdown.truncatedOutputUsd)))}`,
|
|
1057
|
+
);
|
|
1058
|
+
}
|
|
1059
|
+
lines.push('');
|
|
1060
|
+
}
|
|
1061
|
+
const truncationCeiling = report.outputShapes.find((shape) => shape.p95WithinTokens !== null);
|
|
1062
|
+
if (truncationCeiling !== undefined) {
|
|
1063
|
+
lines.push(`_${mdText(t.profile.truncatedCeiling(n(truncationCeiling.p95WithinTokens!)))}_`);
|
|
1064
|
+
lines.push('');
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
/**
|
|
1069
|
+
* This bill against the previous one. The convention prints before the
|
|
1070
|
+
* first figure it governs, and the overlap warning between the figure and
|
|
1071
|
+
* the drivers built from it — the terminal's order, for the terminal's
|
|
1072
|
+
* reason: a caveat below a number is a number somebody already acted on.
|
|
1073
|
+
*/
|
|
1074
|
+
if (against !== undefined) {
|
|
1075
|
+
lines.push(`#### ${t.profile.againstHeading()}`);
|
|
1076
|
+
lines.push('');
|
|
1077
|
+
if (against.nothingPriced) {
|
|
1078
|
+
lines.push(mdText(t.profile.againstNothingPriced()));
|
|
1079
|
+
lines.push('');
|
|
1080
|
+
} else {
|
|
1081
|
+
const delta = report.total.totalUsd - against.previousTotalUsd;
|
|
1082
|
+
const growthPct =
|
|
1083
|
+
against.previousTotalUsd > 0
|
|
1084
|
+
? `${delta >= 0 ? '+' : ''}${((delta / against.previousTotalUsd) * 100).toFixed(1)}%`
|
|
1085
|
+
: '—';
|
|
1086
|
+
lines.push(
|
|
1087
|
+
`**${mdText(t.profile.againstTotals(formatUsd(against.previousTotalUsd), formatUsd(report.total.totalUsd), formatSignedUsd(delta), growthPct, t.profile.calls(against.previousCalls), t.profile.calls(report.total.calls)))}**`,
|
|
1088
|
+
);
|
|
1089
|
+
lines.push('');
|
|
1090
|
+
if (against.overlap !== null) {
|
|
1091
|
+
lines.push(`> ⚠️ ${mdText(t.profile.againstOverlap(against.overlap.from, against.overlap.to))}`);
|
|
1092
|
+
lines.push('');
|
|
1093
|
+
}
|
|
1094
|
+
const describe = (driver: AgainstDriver, shown: string): string =>
|
|
1095
|
+
driver.was === null
|
|
1096
|
+
? t.profile.againstDriverNew(formatSignedUsd(driver.delta), shown)
|
|
1097
|
+
: driver.now === null
|
|
1098
|
+
? t.profile.againstDriverGone(formatSignedUsd(driver.delta), shown)
|
|
1099
|
+
: t.profile.againstDriver(formatSignedUsd(driver.delta), shown, formatUsd(driver.was), formatUsd(driver.now));
|
|
1100
|
+
for (const driver of against.labelDrivers.slice(0, 5)) {
|
|
1101
|
+
lines.push(`- ${mdText(describe(driver, showLabel(driver.key)))}`);
|
|
1102
|
+
}
|
|
1103
|
+
if (against.modelDrivers.length > 0) {
|
|
1104
|
+
lines.push('');
|
|
1105
|
+
lines.push(`_${mdText(t.profile.againstByModel())}_`);
|
|
1106
|
+
for (const driver of against.modelDrivers.slice(0, 3)) {
|
|
1107
|
+
lines.push(`- ${mdText(describe(driver, driver.key))}`);
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
lines.push('');
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
// The provenance caveat before the data gaps: a stale table qualifies every
|
|
1115
|
+
// dollar above, and it does not name its own size the way a skipped line does.
|
|
1116
|
+
if (stalePricing !== undefined) {
|
|
1117
|
+
lines.push(`> ⚠️ ${mdText(t.profile.pricesStale(stalePricing.date, stalePricing.days))}`);
|
|
1118
|
+
lines.push('');
|
|
1119
|
+
}
|
|
1120
|
+
if (report.unpricedModels.length > 0) {
|
|
1121
|
+
lines.push(`> ⚠️ ${mdText(t.profile.unpriced(report.unpricedModels.join(', '), report.unpriced.calls))}`);
|
|
1122
|
+
lines.push('');
|
|
1123
|
+
}
|
|
1124
|
+
if (report.skippedLines.length > 0) {
|
|
1125
|
+
const shown = report.skippedLines.slice(0, 5).join(', ');
|
|
1126
|
+
lines.push(`_${mdText(t.profile.skipped(report.skippedLines.length, report.skippedLines.length > 5 ? `${shown}…` : shown))}_`);
|
|
1127
|
+
lines.push('');
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
return `${lines.join('\n').trimEnd()}\n`;
|
|
1131
|
+
}
|
package/src/time.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The clock's little formatters, shared by the terminal and the markdown
|
|
3
|
+
* renderings so the two cannot drift — the same reason `formatUsd` lives once.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** A gap, in the coarsest unit that keeps one significant figure honest. */
|
|
7
|
+
export function formatGap(ms: number): string {
|
|
8
|
+
if (ms < 90_000) return `${Math.round(ms / 1000)}s`;
|
|
9
|
+
if (ms < 90 * 60_000) return `${Math.round(ms / 60_000)}m`;
|
|
10
|
+
if (ms < 36 * 3_600_000) return `${(ms / 3_600_000).toFixed(1)}h`;
|
|
11
|
+
return `${(ms / 86_400_000).toFixed(1)}d`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** `YYYY-MM-DD`, UTC — the same bucketing the core's spendByDay uses. */
|
|
15
|
+
export const dayOf = (ms: number): string => new Date(ms).toISOString().slice(0, 10);
|
|
16
|
+
|
|
17
|
+
/** The span's length in days, one decimal. */
|
|
18
|
+
export const spanDays = (fromMs: number, toMs: number): string =>
|
|
19
|
+
((toMs - fromMs) / 86_400_000).toFixed(1);
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The median of a list, the yardstick a spike cannot inflate.
|
|
23
|
+
*
|
|
24
|
+
* A mean would let the most expensive day raise the bar it is measured
|
|
25
|
+
* against; the median holds still. Returns 0 for an empty list.
|
|
26
|
+
*/
|
|
27
|
+
export function median(values: readonly number[]): number {
|
|
28
|
+
if (values.length === 0) return 0;
|
|
29
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
30
|
+
const mid = Math.floor(sorted.length / 2);
|
|
31
|
+
return sorted.length % 2 === 1 ? sorted[mid]! : (sorted[mid - 1]! + sorted[mid]!) / 2;
|
|
32
|
+
}
|