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.
@@ -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":"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"}
@@ -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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"}