@trazum/core 1.50.3 → 1.50.5
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/config-schema.d.ts +18 -1
- package/dist/config-schema.d.ts.map +1 -1
- package/dist/config-schema.js +58 -0
- package/dist/config-schema.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/outcome.d.ts +144 -0
- package/dist/outcome.d.ts.map +1 -0
- package/dist/outcome.js +117 -0
- package/dist/outcome.js.map +1 -0
- package/dist/per-outcome.d.ts +129 -0
- package/dist/per-outcome.d.ts.map +1 -0
- package/dist/per-outcome.js +150 -0
- package/dist/per-outcome.js.map +1 -0
- package/dist/usage.d.ts +41 -0
- package/dist/usage.d.ts.map +1 -1
- package/dist/usage.js +85 -1
- package/dist/usage.js.map +1 -1
- package/package.json +1 -1
- package/src/config-schema.ts +97 -0
- package/src/index.ts +23 -0
- package/src/outcome.ts +214 -0
- package/src/per-outcome.ts +235 -0
- package/src/usage.ts +132 -1
package/src/usage.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { createInputShapeTracker } from './input-shape.js';
|
|
|
5
5
|
import { createRepeatsTracker } from './repeats.js';
|
|
6
6
|
import { createTruncationRetryTracker } from './truncation-retry.js';
|
|
7
7
|
import { createTtlFitTracker } from './ttl-fit.js';
|
|
8
|
+
import type { OutcomeTally } from './outcome.js';
|
|
8
9
|
import { createSessionLedgerTracker } from './session-ledger.js';
|
|
9
10
|
import { createSessionCostTracker } from './session-cost.js';
|
|
10
11
|
import type { SessionCostShape } from './session-cost.js';
|
|
@@ -111,6 +112,17 @@ export interface UsageRecord {
|
|
|
111
112
|
* refuses to read a log until it is annotated is a profile nobody runs.
|
|
112
113
|
*/
|
|
113
114
|
label: string | null;
|
|
115
|
+
/**
|
|
116
|
+
* What happened, in the product's own vocabulary — `resolved`, `escalated`,
|
|
117
|
+
* `thumbs-down`, whatever the words are.
|
|
118
|
+
*
|
|
119
|
+
* The counterpart every other figure here has been missing. **Recorded by
|
|
120
|
+
* the caller and never inferred**: no absence of complaint counts as
|
|
121
|
+
* success, no short conversation counts as resolution, no retry counts as
|
|
122
|
+
* failure. `null` is "not recorded" and is reported as such, which is a
|
|
123
|
+
* different answer from any outcome and is treated as one everywhere.
|
|
124
|
+
*/
|
|
125
|
+
outcome: string | null;
|
|
114
126
|
/**
|
|
115
127
|
* Optional conversation identifier, for measuring what re-sent history costs.
|
|
116
128
|
*
|
|
@@ -241,6 +253,8 @@ export interface FieldCoverage {
|
|
|
241
253
|
label: number;
|
|
242
254
|
/** Records with a usable `session` or `conversation_id`. */
|
|
243
255
|
session: number;
|
|
256
|
+
/** Records with a usable `outcome` or `trazum_outcome`. */
|
|
257
|
+
outcome: number;
|
|
244
258
|
/** Records with a readable timestamp. */
|
|
245
259
|
ts: number;
|
|
246
260
|
/** Records with a `stop_reason` or `finish_reason`. */
|
|
@@ -426,6 +440,23 @@ export interface UsageProfileReport {
|
|
|
426
440
|
* and a boolean would call it that.
|
|
427
441
|
*/
|
|
428
442
|
fieldCoverage: FieldCoverage;
|
|
443
|
+
/**
|
|
444
|
+
* The same tally per label, and per model.
|
|
445
|
+
*
|
|
446
|
+
* The groupings a decision is made at. "This workload costs more per
|
|
447
|
+
* resolution than that one" is the finding a total cannot make, and it needs
|
|
448
|
+
* the numerator sliced the same way the bill already is.
|
|
449
|
+
*/
|
|
450
|
+
outcomeTallyByLabel: Array<{ label: string; calls: number; totalUsd: number; tally: OutcomeTally }>;
|
|
451
|
+
outcomeTallyByModel: Array<{ model: string; calls: number; totalUsd: number; tally: OutcomeTally }>;
|
|
452
|
+
/**
|
|
453
|
+
* What each recorded outcome cost — measurement only, no judgement.
|
|
454
|
+
*
|
|
455
|
+
* `outcomeReport` turns this into a success rate where the config declares
|
|
456
|
+
* one, because which values mean success is a product judgement this module
|
|
457
|
+
* has no standing to make. An aggregate, never a list of calls.
|
|
458
|
+
*/
|
|
459
|
+
outcomeTally: OutcomeTally;
|
|
429
460
|
/**
|
|
430
461
|
* Spend per hour of the UTC day, 0–23, over priced records that carry a
|
|
431
462
|
* clock — and only the hours that saw traffic.
|
|
@@ -778,6 +809,13 @@ export function parseUsageLine(line: string): UsageRecord | null {
|
|
|
778
809
|
* nobody sets measures nothing.
|
|
779
810
|
*/
|
|
780
811
|
session: nameOf(record.session) ?? nameOf(record.conversation_id),
|
|
812
|
+
/**
|
|
813
|
+
* Read from either spelling, for the same reason `session` is: `outcome`
|
|
814
|
+
* in a log somebody wrote for this, `trazum_outcome` in one where a
|
|
815
|
+
* namespace was wanted. A field nobody sets measures nothing, and making
|
|
816
|
+
* its adoption a chore is how that happens.
|
|
817
|
+
*/
|
|
818
|
+
outcome: nameOf(record.outcome) ?? nameOf(record.trazum_outcome),
|
|
781
819
|
ts: moment.kind === 'ok' ? moment.ms : null,
|
|
782
820
|
/**
|
|
783
821
|
* Anthropic spells it `stop_reason: "max_tokens"`, OpenAI
|
|
@@ -795,6 +833,28 @@ export function parseUsageLine(line: string): UsageRecord | null {
|
|
|
795
833
|
};
|
|
796
834
|
}
|
|
797
835
|
|
|
836
|
+
/**
|
|
837
|
+
* One slice's outcome tally, in the same shape as the whole log's.
|
|
838
|
+
*
|
|
839
|
+
* `parsed` is the slice's own call count rather than the log's, so a coverage
|
|
840
|
+
* share computed from it describes this slice and not the file it came from.
|
|
841
|
+
*/
|
|
842
|
+
function tallyOf(
|
|
843
|
+
buckets: Map<string, { calls: number; usd: number }> | undefined,
|
|
844
|
+
unrecordedUsd: number,
|
|
845
|
+
parsed: number,
|
|
846
|
+
): OutcomeTally {
|
|
847
|
+
const byValue = [...(buckets ?? new Map()).entries()]
|
|
848
|
+
.map(([value, bucket]) => ({ value, calls: bucket.calls, usd: bucket.usd }))
|
|
849
|
+
.sort((a, b) => b.usd - a.usd);
|
|
850
|
+
return {
|
|
851
|
+
byValue,
|
|
852
|
+
recorded: byValue.reduce((sum, entry) => sum + entry.calls, 0),
|
|
853
|
+
parsed,
|
|
854
|
+
unrecordedUsd,
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
|
|
798
858
|
/**
|
|
799
859
|
* A label or session identifier, from whatever a real log holds.
|
|
800
860
|
*
|
|
@@ -977,7 +1037,14 @@ export function profileUsage(text: string, options: UsageProfileOptions): UsageP
|
|
|
977
1037
|
const ledger = createSessionLedgerTracker({ catalogue, on });
|
|
978
1038
|
const sessionCosts = createSessionCostTracker({ catalogue, on });
|
|
979
1039
|
let hasSessions = false;
|
|
980
|
-
const
|
|
1040
|
+
const outcomes = new Map<string, { calls: number; usd: number }>();
|
|
1041
|
+
let unrecordedOutcomeUsd = 0;
|
|
1042
|
+
/** Per-slice outcome tallies, keyed by label and by model. */
|
|
1043
|
+
const outcomesByLabel = new Map<string, Map<string, { calls: number; usd: number }>>();
|
|
1044
|
+
const outcomesByModel = new Map<string, Map<string, { calls: number; usd: number }>>();
|
|
1045
|
+
const unrecordedByLabel = new Map<string, number>();
|
|
1046
|
+
const unrecordedByModel = new Map<string, number>();
|
|
1047
|
+
const coverage = { label: 0, session: 0, outcome: 0, ts: 0, stopReason: 0, cacheTtl: 0, cacheWrites: 0, parsed: 0 };
|
|
981
1048
|
/**
|
|
982
1049
|
* Raw lines already seen, for the duplicate check. Bounded by the number of
|
|
983
1050
|
* *timestamped* lines — the price of catching a doubled bill, paid only on
|
|
@@ -1040,6 +1107,7 @@ export function profileUsage(text: string, options: UsageProfileOptions): UsageP
|
|
|
1040
1107
|
coverage.parsed += 1;
|
|
1041
1108
|
if (record.label !== null) coverage.label += 1;
|
|
1042
1109
|
if (record.session !== null) coverage.session += 1;
|
|
1110
|
+
if (record.outcome !== null) coverage.outcome += 1;
|
|
1043
1111
|
if (record.ts !== null) coverage.ts += 1;
|
|
1044
1112
|
if (record.truncated !== null) coverage.stopReason += 1;
|
|
1045
1113
|
if (record.cacheWrite5mTokens + record.cacheWrite1hTokens > 0) {
|
|
@@ -1095,6 +1163,41 @@ export function profileUsage(text: string, options: UsageProfileOptions): UsageP
|
|
|
1095
1163
|
if (record.session !== null) {
|
|
1096
1164
|
sessionUsd.set(record.session, (sessionUsd.get(record.session) ?? 0) + (total.totalUsd - usdBefore));
|
|
1097
1165
|
}
|
|
1166
|
+
/**
|
|
1167
|
+
* Tallied from the same per-record dollar as everything else here, so a
|
|
1168
|
+
* success rate by spend and the bill it is a share of can never be two
|
|
1169
|
+
* different arithmetics that drifted apart.
|
|
1170
|
+
*/
|
|
1171
|
+
{
|
|
1172
|
+
const usd = total.totalUsd - usdBefore;
|
|
1173
|
+
if (record.outcome === null) {
|
|
1174
|
+
unrecordedOutcomeUsd += usd;
|
|
1175
|
+
} else {
|
|
1176
|
+
const bucket = outcomes.get(record.outcome) ?? { calls: 0, usd: 0 };
|
|
1177
|
+
bucket.calls += 1;
|
|
1178
|
+
bucket.usd += usd;
|
|
1179
|
+
outcomes.set(record.outcome, bucket);
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
const into = (
|
|
1183
|
+
map: Map<string, Map<string, { calls: number; usd: number }>>,
|
|
1184
|
+
unrecorded: Map<string, number>,
|
|
1185
|
+
key: string,
|
|
1186
|
+
): void => {
|
|
1187
|
+
if (record.outcome === null) {
|
|
1188
|
+
unrecorded.set(key, (unrecorded.get(key) ?? 0) + usd);
|
|
1189
|
+
return;
|
|
1190
|
+
}
|
|
1191
|
+
const slice = map.get(key) ?? new Map<string, { calls: number; usd: number }>();
|
|
1192
|
+
const bucket = slice.get(record.outcome) ?? { calls: 0, usd: 0 };
|
|
1193
|
+
bucket.calls += 1;
|
|
1194
|
+
bucket.usd += usd;
|
|
1195
|
+
slice.set(record.outcome, bucket);
|
|
1196
|
+
map.set(key, slice);
|
|
1197
|
+
};
|
|
1198
|
+
into(outcomesByLabel, unrecordedByLabel, record.label ?? UNLABELLED);
|
|
1199
|
+
into(outcomesByModel, unrecordedByModel, record.model);
|
|
1200
|
+
}
|
|
1098
1201
|
if (record.ts !== null) {
|
|
1099
1202
|
const day = new Date(record.ts).toISOString().slice(0, 10);
|
|
1100
1203
|
const usd = total.totalUsd - usdBefore;
|
|
@@ -1189,6 +1292,34 @@ export function profileUsage(text: string, options: UsageProfileOptions): UsageP
|
|
|
1189
1292
|
}),
|
|
1190
1293
|
duplicateLines: duplicates,
|
|
1191
1294
|
fieldCoverage: coverage,
|
|
1295
|
+
outcomeTally: {
|
|
1296
|
+
byValue: [...outcomes.entries()]
|
|
1297
|
+
.map(([value, bucket]) => ({ value, calls: bucket.calls, usd: bucket.usd }))
|
|
1298
|
+
.sort((a, b) => b.usd - a.usd),
|
|
1299
|
+
recorded: coverage.outcome,
|
|
1300
|
+
parsed: coverage.parsed,
|
|
1301
|
+
unrecordedUsd: unrecordedOutcomeUsd,
|
|
1302
|
+
},
|
|
1303
|
+
outcomeTallyByLabel: sorted(byLabel, 'label').map((entry) => ({
|
|
1304
|
+
label: entry.label,
|
|
1305
|
+
calls: entry.breakdown.calls,
|
|
1306
|
+
totalUsd: entry.breakdown.totalUsd,
|
|
1307
|
+
tally: tallyOf(
|
|
1308
|
+
outcomesByLabel.get(entry.label),
|
|
1309
|
+
unrecordedByLabel.get(entry.label) ?? 0,
|
|
1310
|
+
entry.breakdown.calls,
|
|
1311
|
+
),
|
|
1312
|
+
})),
|
|
1313
|
+
outcomeTallyByModel: sorted(byModel, 'model').map((entry) => ({
|
|
1314
|
+
model: entry.model,
|
|
1315
|
+
calls: entry.breakdown.calls,
|
|
1316
|
+
totalUsd: entry.breakdown.totalUsd,
|
|
1317
|
+
tally: tallyOf(
|
|
1318
|
+
outcomesByModel.get(entry.model),
|
|
1319
|
+
unrecordedByModel.get(entry.model) ?? 0,
|
|
1320
|
+
entry.breakdown.calls,
|
|
1321
|
+
),
|
|
1322
|
+
})),
|
|
1192
1323
|
modelMixDrift: (() => {
|
|
1193
1324
|
const ordered = [...days.entries()].sort((a, b) => a[0].localeCompare(b[0]));
|
|
1194
1325
|
if (ordered.length < 4) return null;
|