margin-meter 0.1.0 → 0.3.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 +353 -8
- package/dist/batching.d.ts +94 -0
- package/dist/batching.d.ts.map +1 -0
- package/dist/batching.js +181 -0
- package/dist/batching.js.map +1 -0
- package/dist/braintrust.d.ts +153 -0
- package/dist/braintrust.d.ts.map +1 -0
- package/dist/braintrust.js +424 -0
- package/dist/braintrust.js.map +1 -0
- package/dist/index.d.ts +428 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +927 -9
- package/dist/index.js.map +1 -1
- package/dist/instrument.d.ts +74 -0
- package/dist/instrument.d.ts.map +1 -0
- package/dist/instrument.js +153 -0
- package/dist/instrument.js.map +1 -0
- package/dist/langsmith.d.ts +150 -0
- package/dist/langsmith.d.ts.map +1 -0
- package/dist/langsmith.js +415 -0
- package/dist/langsmith.js.map +1 -0
- package/dist/promptfoo.d.ts +161 -0
- package/dist/promptfoo.d.ts.map +1 -0
- package/dist/promptfoo.js +446 -0
- package/dist/promptfoo.js.map +1 -0
- package/dist/providers.d.ts +190 -0
- package/dist/providers.d.ts.map +1 -0
- package/dist/providers.js +611 -0
- package/dist/providers.js.map +1 -0
- package/dist/substrate.d.ts +51 -0
- package/dist/substrate.d.ts.map +1 -0
- package/dist/substrate.js +132 -0
- package/dist/substrate.js.map +1 -0
- package/dist/vitest.d.ts +232 -0
- package/dist/vitest.d.ts.map +1 -0
- package/dist/vitest.js +502 -0
- package/dist/vitest.js.map +1 -0
- package/package.json +25 -7
package/dist/batching.js
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The buffer that decides WHEN a metered row goes over the wire.
|
|
3
|
+
*
|
|
4
|
+
* The full contract, with the arithmetic behind every default, is
|
|
5
|
+
* `docs/reference/SDK_BATCHING.md`, and this file is the TS twin of
|
|
6
|
+
* `margin_meter/batching.py` — the two must agree, because they are one contract
|
|
7
|
+
* a customer integrates against, not two implementations.
|
|
8
|
+
*
|
|
9
|
+
* The short version: at 1M metered calls/day, one call per HTTP request is 30M
|
|
10
|
+
* Vercel invocations a month and the same traffic batched at 100 is 300K. 100x on
|
|
11
|
+
* a line item Margin pays. It lives in the SDK rather than the server because
|
|
12
|
+
* only the client can decide not to make the request.
|
|
13
|
+
*
|
|
14
|
+
* ⛔ Two properties are not negotiable, and both are about the customer's process
|
|
15
|
+
* rather than our data:
|
|
16
|
+
*
|
|
17
|
+
* 1. **Nothing blocks the caller.** `submit` pushes onto an array and returns.
|
|
18
|
+
* There is no thread here and none is needed — the flush runs off the caller's
|
|
19
|
+
* await chain, driven by an `unref`'d timer so it never holds a process open.
|
|
20
|
+
* 2. **The queue is BOUNDED and drops the NEWEST.** An unbounded buffer behind a
|
|
21
|
+
* dead endpoint is an OOM with our name on it, and taking down a customer's
|
|
22
|
+
* production agent because our meter was unavailable is the worst thing this
|
|
23
|
+
* SDK could do. Drop-newest leaves a contiguous prefix and a gap at the tail,
|
|
24
|
+
* which reads as "we stopped recording at 14:20"; drop-oldest leaves a hole in
|
|
25
|
+
* the middle of history, which reads as "they spent less that hour" — an
|
|
26
|
+
* under-count in the flattering direction wearing the costume of a measurement.
|
|
27
|
+
*/
|
|
28
|
+
export const BATCH_PATH = "/api/ingest/batch";
|
|
29
|
+
/** Flush on 100 records OR 10 seconds, whichever comes first. The crossover is
|
|
30
|
+
* 100 ÷ 10 = 10 calls/sec: above it size fires first and invocations scale as
|
|
31
|
+
* volume ÷ 100; below it the timer governs and invocations cap at 8,640/day
|
|
32
|
+
* however quiet the customer is. Each trigger covers the other's failure — size
|
|
33
|
+
* alone stalls forever on a low-volume customer, time alone spends a fixed
|
|
34
|
+
* ~518K invocations/month whatever the traffic. */
|
|
35
|
+
export const DEFAULT_MAX_RECORDS = 100;
|
|
36
|
+
export const DEFAULT_FLUSH_MS = 10_000;
|
|
37
|
+
/** ~14 minutes of buffered traffic at 11.6 calls/sec — enough to ride out a
|
|
38
|
+
* deploy or a short outage of ours. A row is 400–1500 bytes, so the ceiling on
|
|
39
|
+
* the host's memory is 4–15 MB, which is the number to quote a customer. */
|
|
40
|
+
export const DEFAULT_MAX_QUEUE = 10_000;
|
|
41
|
+
/** Bounded so a shutdown flush can never hang the host's exit. */
|
|
42
|
+
export const SHUTDOWN_TIMEOUT_MS = 5_000;
|
|
43
|
+
/** Read a positive tunable from the environment, ignoring nonsense.
|
|
44
|
+
*
|
|
45
|
+
* A malformed or non-positive value falls back to the default rather than
|
|
46
|
+
* throwing: a typo in a deployment variable must never stop a customer's process
|
|
47
|
+
* from starting, and a zero flush interval would be a busy loop. */
|
|
48
|
+
function envNumber(name, fallback, minimum) {
|
|
49
|
+
const env = (typeof process !== "undefined" && process.env) || {};
|
|
50
|
+
const raw = env[name];
|
|
51
|
+
if (raw === undefined)
|
|
52
|
+
return fallback;
|
|
53
|
+
const value = Number(raw);
|
|
54
|
+
if (!Number.isFinite(value) || value < minimum)
|
|
55
|
+
return fallback;
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* A bounded queue plus a timer that flushes it on size or time.
|
|
60
|
+
*
|
|
61
|
+
* `send` is awaited off the caller's path and is expected never to reject — it
|
|
62
|
+
* owns the POST, the retries and the accounting.
|
|
63
|
+
*/
|
|
64
|
+
export class Batcher {
|
|
65
|
+
maxRecords;
|
|
66
|
+
flushMs;
|
|
67
|
+
maxQueue;
|
|
68
|
+
queue = [];
|
|
69
|
+
timer = null;
|
|
70
|
+
inFlight = Promise.resolve();
|
|
71
|
+
/** True once `close()` has run. Public so a caller can report a refused emit
|
|
72
|
+
* under the reason that actually caused it: "the meter was shut down" and "the
|
|
73
|
+
* buffer is full" have opposite fixes, and filing one as the other sends the
|
|
74
|
+
* reader hunting for a network problem that is not there. */
|
|
75
|
+
closed = false;
|
|
76
|
+
// ⚠️ Declared rather than written as a constructor parameter property: Node's
|
|
77
|
+
// strip-only TypeScript mode (which this SDK's own test runner uses) rejects
|
|
78
|
+
// parameter properties outright, and a package that cannot be loaded by the
|
|
79
|
+
// runtime its tests run on is not testable.
|
|
80
|
+
send;
|
|
81
|
+
constructor(send, options = {}) {
|
|
82
|
+
this.send = send;
|
|
83
|
+
this.maxRecords = Math.floor(options.maxRecords ?? envNumber("MARGIN_BATCH_MAX_RECORDS", DEFAULT_MAX_RECORDS, 1));
|
|
84
|
+
// The env var is in SECONDS so the two SDKs read the same variable the same
|
|
85
|
+
// way; a deployment should not have to remember which one wants milliseconds.
|
|
86
|
+
this.flushMs =
|
|
87
|
+
options.flushMs ??
|
|
88
|
+
envNumber("MARGIN_BATCH_FLUSH_SECONDS", DEFAULT_FLUSH_MS / 1000, 0.001) * 1000;
|
|
89
|
+
this.maxQueue = Math.floor(options.maxQueue ?? envNumber("MARGIN_BATCH_MAX_QUEUE", DEFAULT_MAX_QUEUE, 1));
|
|
90
|
+
}
|
|
91
|
+
/** Queue one record. `false` when the queue was full and it was DROPPED.
|
|
92
|
+
*
|
|
93
|
+
* ⛔ A bounded push and a size check are the entire cost on the caller's path.
|
|
94
|
+
* Nothing here is awaited. */
|
|
95
|
+
submit(record) {
|
|
96
|
+
if (this.closed)
|
|
97
|
+
return false;
|
|
98
|
+
if (this.queue.length >= this.maxQueue) {
|
|
99
|
+
// Drop the NEWEST — see the module comment. The rows already queued have
|
|
100
|
+
// waited longer and, under a sustained outage, are the ones whose loss
|
|
101
|
+
// would put a hole in the middle of the ledger rather than at its end.
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
this.queue.push(record);
|
|
105
|
+
if (this.queue.length >= this.maxRecords) {
|
|
106
|
+
void this.flush();
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
this.arm();
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
/** Start the interval timer if it is not already running.
|
|
113
|
+
*
|
|
114
|
+
* ⭐ `unref`'d, which is what lets a short-lived script exit on its own
|
|
115
|
+
* schedule instead of waiting out our timer — and what makes `beforeExit`
|
|
116
|
+
* fire at all, since a ref'd timer would keep the event loop alive forever. */
|
|
117
|
+
arm() {
|
|
118
|
+
if (this.timer !== null || this.closed)
|
|
119
|
+
return;
|
|
120
|
+
const timer = setTimeout(() => {
|
|
121
|
+
this.timer = null;
|
|
122
|
+
void this.flush();
|
|
123
|
+
}, this.flushMs);
|
|
124
|
+
if (typeof timer.unref === "function") {
|
|
125
|
+
timer.unref();
|
|
126
|
+
}
|
|
127
|
+
this.timer = timer;
|
|
128
|
+
}
|
|
129
|
+
/** Send everything queued when this was called, and resolve when it has been
|
|
130
|
+
* delivered or counted as dropped. Never rejects. */
|
|
131
|
+
async flush() {
|
|
132
|
+
if (this.timer !== null) {
|
|
133
|
+
clearTimeout(this.timer);
|
|
134
|
+
this.timer = null;
|
|
135
|
+
}
|
|
136
|
+
// Chained rather than parallel: two overlapping flushes would interleave
|
|
137
|
+
// batches and make "everything queued when I called" unanswerable.
|
|
138
|
+
const previous = this.inFlight;
|
|
139
|
+
this.inFlight = (async () => {
|
|
140
|
+
await previous;
|
|
141
|
+
while (this.queue.length > 0) {
|
|
142
|
+
const batch = this.queue.splice(0, this.maxRecords);
|
|
143
|
+
try {
|
|
144
|
+
await this.send(batch);
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
/* `send` owns its own accounting; a throw here must not stop the drain */
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
})();
|
|
151
|
+
await this.inFlight;
|
|
152
|
+
}
|
|
153
|
+
/** Stop accepting, drain what is queued, and return anything left behind.
|
|
154
|
+
*
|
|
155
|
+
* The stranded records are handed BACK rather than swallowed so the caller can
|
|
156
|
+
* count them: an abandoned buffer nobody counts is exactly the invisible loss
|
|
157
|
+
* the delivery accounting exists to make visible. */
|
|
158
|
+
async close(timeoutMs = SHUTDOWN_TIMEOUT_MS) {
|
|
159
|
+
this.closed = true;
|
|
160
|
+
if (this.timer !== null) {
|
|
161
|
+
clearTimeout(this.timer);
|
|
162
|
+
this.timer = null;
|
|
163
|
+
}
|
|
164
|
+
let timer;
|
|
165
|
+
const bounded = new Promise((resolve) => {
|
|
166
|
+
timer = setTimeout(resolve, timeoutMs);
|
|
167
|
+
if (typeof timer.unref === "function") {
|
|
168
|
+
timer.unref();
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
// ⛔ Raced against a timeout so a wedged endpoint can never hang the host's
|
|
172
|
+
// exit. Whatever is still queued when the race ends is returned, not lost.
|
|
173
|
+
await Promise.race([this.flush(), bounded]);
|
|
174
|
+
if (timer !== undefined)
|
|
175
|
+
clearTimeout(timer);
|
|
176
|
+
const stranded = this.queue;
|
|
177
|
+
this.queue = [];
|
|
178
|
+
return stranded;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
//# sourceMappingURL=batching.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"batching.js","sourceRoot":"","sources":["../src/batching.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,MAAM,CAAC,MAAM,UAAU,GAAG,mBAAmB,CAAC;AAE9C;;;;;oDAKoD;AACpD,MAAM,CAAC,MAAM,mBAAmB,GAAG,GAAG,CAAC;AACvC,MAAM,CAAC,MAAM,gBAAgB,GAAG,MAAM,CAAC;AAEvC;;6EAE6E;AAC7E,MAAM,CAAC,MAAM,iBAAiB,GAAG,MAAM,CAAC;AAExC,kEAAkE;AAClE,MAAM,CAAC,MAAM,mBAAmB,GAAG,KAAK,CAAC;AAczC;;;;qEAIqE;AACrE,SAAS,SAAS,CAAC,IAAY,EAAE,QAAgB,EAAE,OAAe;IAChE,MAAM,GAAG,GAAG,CAAC,OAAO,OAAO,KAAK,WAAW,IAAI,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC;IAClE,MAAM,GAAG,GAAG,GAAG,CAAC,IAAI,CAAC,CAAC;IACtB,IAAI,GAAG,KAAK,SAAS;QAAE,OAAO,QAAQ,CAAC;IACvC,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;IAC1B,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,OAAO;QAAE,OAAO,QAAQ,CAAC;IAChE,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;GAKG;AACH,MAAM,OAAO,OAAO;IACT,UAAU,CAAS;IACnB,OAAO,CAAS;IAChB,QAAQ,CAAS;IAClB,KAAK,GAAkB,EAAE,CAAC;IAC1B,KAAK,GAAyC,IAAI,CAAC;IACnD,QAAQ,GAAkB,OAAO,CAAC,OAAO,EAAE,CAAC;IACpD;;;kEAG8D;IAC9D,MAAM,GAAG,KAAK,CAAC;IACf,8EAA8E;IAC9E,6EAA6E;IAC7E,4EAA4E;IAC5E,4CAA4C;IAC3B,IAAI,CAA0C;IAE/D,YACE,IAA6C,EAC7C,UAA0B,EAAE;QAE5B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,KAAK,CAC1B,OAAO,CAAC,UAAU,IAAI,SAAS,CAAC,0BAA0B,EAAE,mBAAmB,EAAE,CAAC,CAAC,CACpF,CAAC;QACF,4EAA4E;QAC5E,8EAA8E;QAC9E,IAAI,CAAC,OAAO;YACV,OAAO,CAAC,OAAO;gBACf,SAAS,CAAC,4BAA4B,EAAE,gBAAgB,GAAG,IAAI,EAAE,KAAK,CAAC,GAAG,IAAI,CAAC;QACjF,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,KAAK,CACxB,OAAO,CAAC,QAAQ,IAAI,SAAS,CAAC,wBAAwB,EAAE,iBAAiB,EAAE,CAAC,CAAC,CAC9E,CAAC;IACJ,CAAC;IAED;;;mCAG+B;IAC/B,MAAM,CAAC,MAAmB;QACxB,IAAI,IAAI,CAAC,MAAM;YAAE,OAAO,KAAK,CAAC;QAC9B,IAAI,IAAI,CAAC,KAAK,CAAC,MAAM,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YACvC,yEAAyE;YACzE,uEAAuE;YACvE,uEAAuE;YACvE,OAAO,KAAK,CAAC;QACf,CAAC;QACD,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACxB,IAAI,IAAI,CAAC,KAAK,CAAC,MAAM,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;YACzC,KAAK,IAAI,CAAC,KAAK,EAAE,CAAC;YAClB,OAAO,IAAI,CAAC;QACd,CAAC;QACD,IAAI,CAAC,GAAG,EAAE,CAAC;QACX,OAAO,IAAI,CAAC;IACd,CAAC;IAED;;;;oFAIgF;IACxE,GAAG;QACT,IAAI,IAAI,CAAC,KAAK,KAAK,IAAI,IAAI,IAAI,CAAC,MAAM;YAAE,OAAO;QAC/C,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;YAC5B,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;YAClB,KAAK,IAAI,CAAC,KAAK,EAAE,CAAC;QACpB,CAAC,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;QACjB,IAAI,OAAQ,KAAgC,CAAC,KAAK,KAAK,UAAU,EAAE,CAAC;YACjE,KAA+B,CAAC,KAAK,EAAE,CAAC;QAC3C,CAAC;QACD,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;IACrB,CAAC;IAED;0DACsD;IACtD,KAAK,CAAC,KAAK;QACT,IAAI,IAAI,CAAC,KAAK,KAAK,IAAI,EAAE,CAAC;YACxB,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACzB,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;QACpB,CAAC;QACD,yEAAyE;QACzE,mEAAmE;QACnE,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC;QAC/B,IAAI,CAAC,QAAQ,GAAG,CAAC,KAAK,IAAI,EAAE;YAC1B,MAAM,QAAQ,CAAC;YACf,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAC7B,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC;gBACpD,IAAI,CAAC;oBACH,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;gBACzB,CAAC;gBAAC,MAAM,CAAC;oBACP,0EAA0E;gBAC5E,CAAC;YACH,CAAC;QACH,CAAC,CAAC,EAAE,CAAC;QACL,MAAM,IAAI,CAAC,QAAQ,CAAC;IACtB,CAAC;IAED;;;;0DAIsD;IACtD,KAAK,CAAC,KAAK,CAAC,YAAoB,mBAAmB;QACjD,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;QACnB,IAAI,IAAI,CAAC,KAAK,KAAK,IAAI,EAAE,CAAC;YACxB,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACzB,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;QACpB,CAAC;QACD,IAAI,KAAgD,CAAC;QACrD,MAAM,OAAO,GAAG,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE;YAC5C,KAAK,GAAG,UAAU,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;YACvC,IAAI,OAAQ,KAAgC,CAAC,KAAK,KAAK,UAAU,EAAE,CAAC;gBACjE,KAA+B,CAAC,KAAK,EAAE,CAAC;YAC3C,CAAC;QACH,CAAC,CAAC,CAAC;QACH,2EAA2E;QAC3E,2EAA2E;QAC3E,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,KAAK,EAAE,EAAE,OAAO,CAAC,CAAC,CAAC;QAC5C,IAAI,KAAK,KAAK,SAAS;YAAE,YAAY,CAAC,KAAK,CAAC,CAAC;QAC7C,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC;QAC5B,IAAI,CAAC,KAAK,GAAG,EAAE,CAAC;QAChB,OAAO,QAAQ,CAAC;IAClB,CAAC;CACF"}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* margin-meter/braintrust — turn a Braintrust eval result file into outcomes.
|
|
3
|
+
*
|
|
4
|
+
* The TypeScript twin of `sdk/python/margin_meter/braintrust.py`. Braintrust is
|
|
5
|
+
* the eval platform funded AI teams most often already pay for, and the nearest
|
|
6
|
+
* competitor Margin has (`docs/growth/POSITIONING.md`): it ships "cost per
|
|
7
|
+
* successful eval" — the metric — but has no automated parity-gated
|
|
8
|
+
* promote/revert — the action. So this adapter is the neutral-auditor wedge in
|
|
9
|
+
* code: it hands a Braintrust customer the action layer they lack, over the
|
|
10
|
+
* evals they already run. Rung 2 of `docs/INTEGRATION_LADDER.md`.
|
|
11
|
+
*
|
|
12
|
+
* import { MarginMeter, braintrust } from "margin-meter";
|
|
13
|
+
* const meter = new MarginMeter();
|
|
14
|
+
* const data = JSON.parse(await readFile("results.json", "utf8"));
|
|
15
|
+
* await braintrust.recordResults(meter, data, "my-suite", { threshold: 0.7 });
|
|
16
|
+
*
|
|
17
|
+
* The mapping is identical to the Python adapter's, on purpose — one Braintrust
|
|
18
|
+
* result → one outcome:
|
|
19
|
+
*
|
|
20
|
+
* - `qualityScore` ← the mean of the case's numeric scores in [0, 1] (Braintrust
|
|
21
|
+
* reports a named score per scorer, not one per case).
|
|
22
|
+
* - `passed` ← Braintrust has NO intrinsic pass/fail (its scores are
|
|
23
|
+
* continuous), so it is derived HONESTLY, never fabricated: (1) `threshold`
|
|
24
|
+
* given → mean ≥ threshold; (2) no threshold but every scorer is binary
|
|
25
|
+
* (0.0/1.0) → every scorer passed; (3) no threshold and continuous scores →
|
|
26
|
+
* the row is SKIPPED with a warning.
|
|
27
|
+
* - `qualityMethod`/`graderKind` ← DERIVED from the scorer names, never assumed
|
|
28
|
+
* (weaker-claim-wins, see the honesty rule below).
|
|
29
|
+
* - `judgeModelId` ← an opt-in `margin_judge_model` field, ridden only when the
|
|
30
|
+
* grade is an LLM judge (rung 6 provenance). A bare `model` field is NOT used —
|
|
31
|
+
* in a Braintrust record it is usually the generation model under test.
|
|
32
|
+
* - `link` ← the case id/span_id/root_span_id, else `braintrust:<index>` —
|
|
33
|
+
* never its `input` (MAR-1548).
|
|
34
|
+
* - `valueUsd` ← opt-in via `metadata.margin_value` (rung 8); absent is the
|
|
35
|
+
* cost-per-outcome default, never 0.
|
|
36
|
+
* - a result with no numeric score at all is SKIPPED — recording a non-verdict
|
|
37
|
+
* would fabricate an outcome the eval did not produce.
|
|
38
|
+
*
|
|
39
|
+
* ## The honesty rule
|
|
40
|
+
*
|
|
41
|
+
* Braintrust grades two ways and the adapter must not conflate them: code-graded
|
|
42
|
+
* scorers (ExactMatch, Levenshtein, NumericDiff, JSONDiff, …) are
|
|
43
|
+
* `ground_truth`/`code`; model-graded autoevals (Factuality, ClosedQA,
|
|
44
|
+
* AnswerRelevancy, Moderation, …) are `llm_judge`/`judge`. The WEAKER honest
|
|
45
|
+
* claim wins: any model-graded scorer makes the whole outcome a judge grade; a
|
|
46
|
+
* code scorer mixed with an unrecognised one is downgraded to `unknown`; nothing
|
|
47
|
+
* recognisable is `unknown`.
|
|
48
|
+
*
|
|
49
|
+
* Same two promises as the promptfoo adapter: it consumes a file the customer
|
|
50
|
+
* already produces, and it never fabricates and is fail-safe.
|
|
51
|
+
*/
|
|
52
|
+
import type { RecordOutcomeInput } from "./index.ts";
|
|
53
|
+
/** Where a warning goes. Defaults to `process.emitWarning`; a test injects a
|
|
54
|
+
* collector. */
|
|
55
|
+
export type WarnFn = (message: string) => void;
|
|
56
|
+
/** Grading provenance for one outcome. */
|
|
57
|
+
export interface GraderSignature {
|
|
58
|
+
qualityMethod: string;
|
|
59
|
+
graderKind: string;
|
|
60
|
+
}
|
|
61
|
+
/** Normalise a scorer name for set membership: lower-case, keep only
|
|
62
|
+
* alphanumerics (so `Answer Relevancy`/`answer_relevancy` collapse), strip a
|
|
63
|
+
* leading `not` negation. Returns undefined for a missing/blank name. */
|
|
64
|
+
export declare function normScorerName(raw: unknown): string | undefined;
|
|
65
|
+
/** The per-scorer numeric scores of a case, as `{ [scorer]: score }`. Braintrust
|
|
66
|
+
* records `scores` either as a mapping (`{name: score|null}`) or a list of
|
|
67
|
+
* `{name, score}`. Both are read; a null/non-numeric/out-of-[0,1] score is
|
|
68
|
+
* dropped (the ingest layer rejects a score outside [0,1] and a lost decimal
|
|
69
|
+
* beats a lost row). */
|
|
70
|
+
export declare function scoresOf(result: unknown): Record<string, number>;
|
|
71
|
+
/** Derive `(qualityMethod, graderKind)` from the scorer names, weaker-claim-wins:
|
|
72
|
+
* - any model-graded scorer → llm_judge/judge
|
|
73
|
+
* - only recognised code scorers → ground_truth/code
|
|
74
|
+
* - anything else → unknown/unknown */
|
|
75
|
+
export declare function scorerSignature(scorerNames: Iterable<unknown>): GraderSignature;
|
|
76
|
+
/** The judging model, from an explicit `margin_judge_model` opt-in on the result
|
|
77
|
+
* or its metadata, or undefined. A bare `model` field is deliberately NOT used. */
|
|
78
|
+
export declare function judgeModelFromResult(result: unknown): string | undefined;
|
|
79
|
+
/** Read an opt-in `margin_value` off a result's `metadata` (or a top-level key)
|
|
80
|
+
* — the value-per-outcome numerator (rung 8). Returns the USD amount, or
|
|
81
|
+
* undefined when absent. Throws on a malformed/implausible value so the
|
|
82
|
+
* caller can warn and drop it. */
|
|
83
|
+
export declare function valueFromResult(result: unknown): number | undefined;
|
|
84
|
+
/** A traceable link back to the exact eval case: its id/span id, else the row
|
|
85
|
+
* index. ⛔ Never the case's `input` (MAR-1548): /security says no prompt
|
|
86
|
+
* reaches us. */
|
|
87
|
+
export declare function linkFromResult(result: unknown, index: number): string;
|
|
88
|
+
/** Derive the pass/fail for a case, or undefined when it cannot be derived
|
|
89
|
+
* honestly (no scores, or continuous scores with no threshold). */
|
|
90
|
+
export declare function passedFromScores(scores: Record<string, number>, threshold: number | undefined): boolean | undefined;
|
|
91
|
+
/** Fields for {@link outcomeInput}. */
|
|
92
|
+
export interface OutcomeInputArgs {
|
|
93
|
+
workflowId: string;
|
|
94
|
+
passed: boolean;
|
|
95
|
+
qualityScore: number;
|
|
96
|
+
scorerNames: Iterable<unknown>;
|
|
97
|
+
link: string;
|
|
98
|
+
judgeModelId?: string;
|
|
99
|
+
valueUsd?: number;
|
|
100
|
+
isSimulated?: boolean;
|
|
101
|
+
}
|
|
102
|
+
/** Translate one graded Braintrust result into `MarginMeter.recordOutcome` input.
|
|
103
|
+
* `judgeModelId` rides only when the grade is an LLM judge and a model was named;
|
|
104
|
+
* `valueUsd` rides only when a value was supplied. */
|
|
105
|
+
export declare function outcomeInput(args: OutcomeInputArgs): RecordOutcomeInput;
|
|
106
|
+
/** Options shared by the result-level translators. */
|
|
107
|
+
export interface TranslateOptions {
|
|
108
|
+
workflowId: string;
|
|
109
|
+
index?: number;
|
|
110
|
+
threshold?: number;
|
|
111
|
+
isSimulated?: boolean;
|
|
112
|
+
onWarn?: WarnFn;
|
|
113
|
+
}
|
|
114
|
+
/** Full translation of one Braintrust result, or undefined if it is not a real
|
|
115
|
+
* verdict (no numeric score, or an ambiguous continuous score with no
|
|
116
|
+
* threshold). A malformed `margin_value` warns and drops the value while keeping
|
|
117
|
+
* the pass/fail. */
|
|
118
|
+
export declare function resultToOutcomeInput(result: unknown, opts: TranslateOptions): RecordOutcomeInput | undefined;
|
|
119
|
+
/** Pull the flat list of per-case results out of parsed Braintrust output. It
|
|
120
|
+
* reaches a file several ways: a bare list, the `EvalResultWithSummary` envelope
|
|
121
|
+
* (`{summary, results}`), a `{results}` wrapper, or the experiment-fetch export
|
|
122
|
+
* (`{events}`). This walks the known shapes rather than pinning one. */
|
|
123
|
+
export declare function extractResults(data: unknown): Record<string, unknown>[];
|
|
124
|
+
/** Yield `recordOutcome` inputs for every real verdict in parsed Braintrust
|
|
125
|
+
* output (any envelope shape {@link extractResults} understands). */
|
|
126
|
+
export declare function iterOutcomes(data: unknown, opts: {
|
|
127
|
+
workflowId: string;
|
|
128
|
+
threshold?: number;
|
|
129
|
+
isSimulated?: boolean;
|
|
130
|
+
onWarn?: WarnFn;
|
|
131
|
+
}): RecordOutcomeInput[];
|
|
132
|
+
/** The minimal meter surface {@link recordResults} needs — satisfied by
|
|
133
|
+
* `MarginMeter`, kept as a structural type so this module imports no runtime
|
|
134
|
+
* value from `index.ts`. */
|
|
135
|
+
export interface OutcomeSink {
|
|
136
|
+
recordOutcome(input: RecordOutcomeInput): Promise<unknown>;
|
|
137
|
+
flush(): Promise<void>;
|
|
138
|
+
}
|
|
139
|
+
/** How one `recordResults` batch landed. */
|
|
140
|
+
export interface RecordSummary {
|
|
141
|
+
recorded: number;
|
|
142
|
+
errors: number;
|
|
143
|
+
skipped: number;
|
|
144
|
+
}
|
|
145
|
+
/** Emit an outcome per verdict in parsed Braintrust output through `meter`.
|
|
146
|
+
* Fail-safe: a delivery fault on one row warns and counts an error; it never
|
|
147
|
+
* aborts the batch. Non-verdict rows are counted as skipped. */
|
|
148
|
+
export declare function recordResults(meter: OutcomeSink, data: unknown, workflowId: string, opts?: {
|
|
149
|
+
threshold?: number;
|
|
150
|
+
isSimulated?: boolean;
|
|
151
|
+
onWarn?: WarnFn;
|
|
152
|
+
}): Promise<RecordSummary>;
|
|
153
|
+
//# sourceMappingURL=braintrust.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"braintrust.d.ts","sourceRoot":"","sources":["../src/braintrust.ts"],"names":[],"mappings":"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"}
|