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,446 @@
1
+ /**
2
+ * margin-meter/promptfoo — turn a `promptfoo eval` result file into outcomes.
3
+ *
4
+ * The TypeScript twin of `sdk/python/margin_meter/promptfoo.py`. promptfoo is a
5
+ * Node tool, so a promptfoo user is already in the JS toolchain and this SDK is
6
+ * their natural bridge. `promptfoo eval -o results.json` writes a code-graded
7
+ * pass/fail file that maps one-to-one onto `POST /api/ingest/outcomes`
8
+ * (`docs/INTEGRATION_LADDER.md` rung 2), so the whole of rung 2 — "their evals
9
+ * report pass/fail" — is available with no hand-written POST:
10
+ *
11
+ * promptfoo eval -o results.json
12
+ *
13
+ * import { MarginMeter, promptfoo } from "margin-meter";
14
+ * const meter = new MarginMeter(); // MARGIN_INGEST_URL/KEY from env
15
+ * const data = JSON.parse(await readFile("results.json", "utf8"));
16
+ * await promptfoo.recordResults(meter, data, "my-eval-suite");
17
+ *
18
+ * The mapping is identical to the Python adapter's, on purpose — the two SDKs
19
+ * must not drift:
20
+ *
21
+ * - `passed` ← `success` (the top-level aggregate verdict).
22
+ * - `qualityScore` ← `score` passed through when in [0, 1]; a score outside
23
+ * that range (a raw metric from a custom assert) falls back to the coherent
24
+ * binary, because the ingest layer rejects an out-of-range score and losing
25
+ * the row is worse than losing the decimal. Always supplied (1.0/0.0 at
26
+ * least): the ingest layer discards `quality_method`/`grader_kind` on a
27
+ * scoreless outcome, so the score is what lights the grading provenance.
28
+ * - `qualityMethod`/`graderKind` ← DERIVED from the assert types, never
29
+ * assumed (see the honesty rule below).
30
+ * - `link` ← `testCase.description`, else the prompt id + index — never
31
+ * `prompt.label`, which promptfoo defaults to the prompt text (MAR-1548).
32
+ * - `valueUsd` ← opt-in via `vars.margin_value` or
33
+ * `testCase.metadata.margin_value` — the only route to value-per-outcome
34
+ * (rung 8). Absent is the cost-per-outcome default, never coerced to 0.
35
+ * - a result with no boolean `success` is not a verdict and is SKIPPED —
36
+ * recording a non-verdict would fabricate an outcome the eval did not
37
+ * produce.
38
+ *
39
+ * ## The honesty rule
40
+ *
41
+ * promptfoo grades two different ways and the adapter must not conflate them:
42
+ * code-graded asserts (`contains`, `equals`, `is-json`, `javascript`, `regex`,
43
+ * …) are `ground_truth`/`code`; model-graded asserts (`llm-rubric`,
44
+ * `factuality`, `similar`, `classifier`, `g-eval`, …) are `llm_judge`/`judge`.
45
+ * The WEAKER honest claim wins: any model-graded assert makes the whole outcome
46
+ * a judge grade; a code assert mixed with an assert type we do not recognise is
47
+ * downgraded to `unknown` (we cannot prove the unknown one was code); nothing
48
+ * recognisable at all is `unknown`. A rubric-graded eval reported as a
49
+ * ground-truth code grade would be the exact productivity illusion Margin
50
+ * exists to puncture.
51
+ *
52
+ * Two design promises, same as the Python adapter: it consumes the file the
53
+ * customer already produces (no new harness), and it never fabricates and is
54
+ * fail-safe — a delivery fault on one row warns and is counted, never aborting
55
+ * the batch.
56
+ */
57
+ // Grading provenance strings — mirror `src/margin/grading.py`, kept as literals
58
+ // so the SDK has no dependency on the server package.
59
+ const GROUND_TRUTH = "ground_truth";
60
+ const LLM_JUDGE = "llm_judge";
61
+ const UNKNOWN_METHOD = "unknown";
62
+ const GRADER_CODE = "code";
63
+ const GRADER_JUDGE = "judge";
64
+ const GRADER_UNKNOWN = "unknown";
65
+ // `Outcome.link` is a `String(512)`; a longer link is truncated, not dropped (a
66
+ // lossy link beats a lost outcome) — same rule as the pytest adapter.
67
+ const MAX_LINK_LEN = 512;
68
+ const VALUE_KEY = "margin_value";
69
+ /** Mirror of `src/margin/ingest.MAX_VALUE_USD`: ingest refuses a value past this. */
70
+ const MAX_VALUE_USD = 10_000_000;
71
+ /** promptfoo assert types that are pure code: a deterministic comparison with no
72
+ * model in the loop. Normalised lower-case, separators stripped so `is-json`
73
+ * and `isjson` collapse together and the `not-` negation prefix is ignored. */
74
+ const CODE_ASSERTS = new Set([
75
+ "equals",
76
+ "contains",
77
+ "icontains",
78
+ "containsall",
79
+ "containsany",
80
+ "icontainsall",
81
+ "icontainsany",
82
+ "startswith",
83
+ "regex",
84
+ "isjson",
85
+ "containsjson",
86
+ "issql",
87
+ "containssql",
88
+ "isxml",
89
+ "containsxml",
90
+ "isvalidopenaifunctioncall",
91
+ "isvalidopenaitoolscall",
92
+ "javascript",
93
+ "python",
94
+ "webhook",
95
+ "levenshtein",
96
+ "rougen",
97
+ "bleu",
98
+ "gleu",
99
+ "meteor",
100
+ "latency",
101
+ "cost",
102
+ "perplexity",
103
+ "perplexityscore",
104
+ "finishreason",
105
+ "guardrails",
106
+ "trace",
107
+ ]);
108
+ /** promptfoo assert types that put a MODEL in the grading loop — an LLM judge,
109
+ * an embedding-similarity model, or a classifier. Any one present means the
110
+ * strongest honest claim is `llm_judge`/`judge`, never a code grade. */
111
+ const MODEL_ASSERTS = new Set([
112
+ "llmrubric",
113
+ "modelgradedclosedqa",
114
+ "modelgradedfactuality",
115
+ "factuality",
116
+ "answerrelevance",
117
+ "contextrelevance",
118
+ "contextfaithfulness",
119
+ "contextrecall",
120
+ "similar",
121
+ "classifier",
122
+ "moderation",
123
+ "selectbest",
124
+ "pi",
125
+ "geval",
126
+ "gevalfactuality",
127
+ ]);
128
+ const defaultWarn = (message) => {
129
+ // `process` may be absent in a non-Node runtime; fall back to console.
130
+ const p = globalThis
131
+ .process;
132
+ if (p && typeof p.emitWarning === "function")
133
+ p.emitWarning(message);
134
+ else
135
+ console.warn(message);
136
+ };
137
+ function isObject(x) {
138
+ return typeof x === "object" && x !== null && !Array.isArray(x);
139
+ }
140
+ /** Normalise a promptfoo assert type for set membership: lower-case, drop
141
+ * dashes/underscores/spaces, strip a leading `not` negation (`not-contains`
142
+ * grades the same WAY as `contains`). Returns undefined for a missing/blank
143
+ * type. */
144
+ export function normAssertType(raw) {
145
+ if (typeof raw !== "string")
146
+ return undefined;
147
+ let t = raw.trim().toLowerCase().replace(/[-_ ]/g, "");
148
+ if (!t)
149
+ return undefined;
150
+ if (t.startsWith("not") && t !== "not")
151
+ t = t.slice(3);
152
+ return t || undefined;
153
+ }
154
+ /** Derive `(qualityMethod, graderKind)` from the assert types behind a result,
155
+ * applying the weaker-claim-wins honesty rule:
156
+ * - any model-graded assert → llm_judge/judge
157
+ * - only recognised code asserts → ground_truth/code
158
+ * - anything else (no asserts, or a code assert mixed with an unrecognised
159
+ * type we cannot vouch for) → unknown/unknown */
160
+ export function gradingSignature(assertTypes) {
161
+ let hasModel = false;
162
+ let hasCode = false;
163
+ let hasUnknown = false;
164
+ for (const raw of assertTypes) {
165
+ const t = normAssertType(raw);
166
+ if (t === undefined)
167
+ continue;
168
+ if (MODEL_ASSERTS.has(t))
169
+ hasModel = true;
170
+ else if (CODE_ASSERTS.has(t))
171
+ hasCode = true;
172
+ else
173
+ hasUnknown = true;
174
+ }
175
+ if (hasModel)
176
+ return { qualityMethod: LLM_JUDGE, graderKind: GRADER_JUDGE };
177
+ if (hasCode && !hasUnknown)
178
+ return { qualityMethod: GROUND_TRUTH, graderKind: GRADER_CODE };
179
+ return { qualityMethod: UNKNOWN_METHOD, graderKind: GRADER_UNKNOWN };
180
+ }
181
+ /** Collect the assert types behind one promptfoo result. Prefers
182
+ * `gradingResult.componentResults[].assertion.type` (what actually ran,
183
+ * including expanded asserts) and its single top-level `assertion`; falls back
184
+ * to the configured `testCase.assert[].type`. */
185
+ export function assertTypesFromResult(result) {
186
+ const types = [];
187
+ if (!isObject(result))
188
+ return types;
189
+ const grading = result.gradingResult;
190
+ if (isObject(grading)) {
191
+ const components = grading.componentResults;
192
+ if (Array.isArray(components)) {
193
+ for (const comp of components) {
194
+ if (isObject(comp) && isObject(comp.assertion)) {
195
+ types.push(comp.assertion.type);
196
+ }
197
+ }
198
+ }
199
+ if (isObject(grading.assertion))
200
+ types.push(grading.assertion.type);
201
+ }
202
+ if (types.length > 0)
203
+ return types;
204
+ const testCase = result.testCase;
205
+ if (isObject(testCase) && Array.isArray(testCase.assert)) {
206
+ for (const a of testCase.assert) {
207
+ if (isObject(a))
208
+ types.push(a.type);
209
+ }
210
+ }
211
+ return types;
212
+ }
213
+ // A finite DECIMAL number, the only string shape accepted. Deliberately narrower
214
+ // than JS `Number()`, which also parses `0x10`/`0o7`/`0b1`/`Infinity`/`""` — a
215
+ // `margin_value` typo like `"0x10"` must be a DROPPED value, never a silent $16.
216
+ // This is the acceptance set of Python `float()` minus `inf`/`nan`, so the two
217
+ // SDKs treat a stringly-typed tag the same and neither lets a non-number through.
218
+ const DECIMAL_RE = /^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/;
219
+ /** Coerce a raw `margin_value` tag to a finite number, tolerating a stringly-typed
220
+ * var. A negative is a loss and is kept (MAR-1198). Throws on a bool, a non-decimal
221
+ * string, a non-finite value, or one past +/-MAX_VALUE_USD — the caller warns and
222
+ * drops the value while keeping the pass/fail. */
223
+ function coerceValue(raw) {
224
+ if (typeof raw === "boolean")
225
+ throw new Error(`${VALUE_KEY} must be a number, got ${JSON.stringify(raw)}`);
226
+ let val;
227
+ if (typeof raw === "number") {
228
+ val = raw;
229
+ }
230
+ else if (typeof raw === "string") {
231
+ const s = raw.trim();
232
+ if (!DECIMAL_RE.test(s))
233
+ throw new Error(`${VALUE_KEY} must be a number, got ${JSON.stringify(raw)}`);
234
+ val = Number(s);
235
+ }
236
+ else {
237
+ throw new Error(`${VALUE_KEY} must be a number, got ${JSON.stringify(raw)}`);
238
+ }
239
+ if (!Number.isFinite(val))
240
+ throw new Error(`${VALUE_KEY} must be a finite number, got ${JSON.stringify(raw)}`);
241
+ // A loss is a real value (MAR-1198): ingest takes any finite amount within +/-MAX.
242
+ if (Math.abs(val) > MAX_VALUE_USD)
243
+ throw new Error(`${VALUE_KEY} implausible (|value| > ${MAX_VALUE_USD}), got ${val}`);
244
+ return val;
245
+ }
246
+ /** Read an opt-in `margin_value` off a result's `vars` or its test case's
247
+ * `metadata`/`vars` — the value-per-outcome numerator (rung 8). Returns the USD
248
+ * amount, or undefined when absent (the normal cost-per-outcome case). Throws
249
+ * on a malformed value so the caller can warn and drop it. */
250
+ export function valueFromResult(result) {
251
+ if (!isObject(result))
252
+ return undefined;
253
+ let raw = undefined;
254
+ const vars = result.vars;
255
+ if (isObject(vars) && VALUE_KEY in vars) {
256
+ raw = vars[VALUE_KEY];
257
+ }
258
+ else {
259
+ const testCase = result.testCase;
260
+ if (isObject(testCase)) {
261
+ const meta = testCase.metadata;
262
+ if (isObject(meta) && VALUE_KEY in meta)
263
+ raw = meta[VALUE_KEY];
264
+ const tv = testCase.vars;
265
+ // `== null` (not `=== undefined`) so an explicit `null` in metadata falls
266
+ // through to testCase.vars, matching the Python adapter's `raw is None`.
267
+ if (raw == null && isObject(tv) && VALUE_KEY in tv)
268
+ raw = tv[VALUE_KEY];
269
+ }
270
+ }
271
+ if (raw === undefined || raw === null)
272
+ return undefined;
273
+ return coerceValue(raw);
274
+ }
275
+ /** A traceable link back to the exact eval case: the test description if the
276
+ * author wrote one, else the prompt id plus the row index. ⛔ Never
277
+ * `prompt.label` (MAR-1548): promptfoo defaults it to the raw prompt text. */
278
+ export function linkFromResult(result, index) {
279
+ if (isObject(result)) {
280
+ const testCase = result.testCase;
281
+ if (isObject(testCase) && typeof testCase.description === "string") {
282
+ const desc = testCase.description.trim();
283
+ if (desc)
284
+ return desc.slice(0, MAX_LINK_LEN);
285
+ }
286
+ // `||` (not `??`) so an empty-string label falls through to the next
287
+ // candidate, matching the Python adapter's `or` chain.
288
+ let label = undefined;
289
+ const prompt = result.prompt;
290
+ if (isObject(prompt))
291
+ label = prompt.id;
292
+ if (!label) {
293
+ label = result.promptId || result.provider;
294
+ if (isObject(label))
295
+ label = label.id || label.label;
296
+ }
297
+ if (typeof label === "string" && label)
298
+ return `${label}:${index}`.slice(0, MAX_LINK_LEN);
299
+ if (typeof label === "number")
300
+ return `${label}:${index}`.slice(0, MAX_LINK_LEN);
301
+ }
302
+ return `promptfoo:${index}`;
303
+ }
304
+ /** Return the `{ passed, score }` verdict for a real result, else undefined.
305
+ * `passed` comes from a boolean `success`; a result without one is not a
306
+ * verdict. `score` is passed through when in [0, 1]; out of that range (a raw
307
+ * metric) falls back to the coherent binary and warns, because the ingest layer
308
+ * rejects an out-of-range score and a lost row is worse than a lost decimal. */
309
+ export function passedAndScore(result, onWarn = defaultWarn) {
310
+ if (!isObject(result))
311
+ return undefined;
312
+ const success = result.success;
313
+ if (typeof success !== "boolean")
314
+ return undefined;
315
+ const passed = success;
316
+ const score = result.score;
317
+ if (typeof score !== "number" || Number.isNaN(score)) {
318
+ return { passed, score: passed ? 1.0 : 0.0 };
319
+ }
320
+ if (score >= 0.0 && score <= 1.0)
321
+ return { passed, score };
322
+ onWarn(`promptfoo score ${score} is outside [0, 1]; recording the binary ` +
323
+ "pass/fail instead so the outcome is not dropped");
324
+ return { passed, score: passed ? 1.0 : 0.0 };
325
+ }
326
+ /** Translate one graded promptfoo result into `MarginMeter.recordOutcome` input.
327
+ * The grader signature is derived from `assertTypes` (never assumed);
328
+ * `valueUsd` rides only when a value was supplied. */
329
+ export function outcomeInput(args) {
330
+ const { qualityMethod, graderKind } = gradingSignature(args.assertTypes);
331
+ const input = {
332
+ workflowId: args.workflowId,
333
+ passed: Boolean(args.passed),
334
+ qualityScore: Number(args.qualityScore),
335
+ qualityMethod,
336
+ graderKind,
337
+ link: String(args.link).slice(0, MAX_LINK_LEN),
338
+ isSimulated: Boolean(args.isSimulated),
339
+ };
340
+ if (args.valueUsd !== undefined)
341
+ input.valueUsd = Number(args.valueUsd);
342
+ return input;
343
+ }
344
+ /** Full translation of one promptfoo result, or undefined if it is not a real
345
+ * verdict. A malformed `margin_value` warns and drops the value (keeping the
346
+ * pass/fail), never throwing into the caller. */
347
+ export function resultToOutcomeInput(result, opts) {
348
+ const onWarn = opts.onWarn ?? defaultWarn;
349
+ const index = opts.index ?? 0;
350
+ const verdict = passedAndScore(result, onWarn);
351
+ if (verdict === undefined)
352
+ return undefined;
353
+ let value;
354
+ try {
355
+ value = valueFromResult(result);
356
+ }
357
+ catch (exc) {
358
+ value = undefined;
359
+ onWarn(`${linkFromResult(result, index)}: ${exc.message}`);
360
+ }
361
+ return outcomeInput({
362
+ workflowId: opts.workflowId,
363
+ passed: verdict.passed,
364
+ qualityScore: verdict.score,
365
+ assertTypes: assertTypesFromResult(result),
366
+ link: linkFromResult(result, index),
367
+ valueUsd: value,
368
+ isSimulated: opts.isSimulated,
369
+ });
370
+ }
371
+ /** Pull the flat list of per-test results out of a parsed promptfoo output.
372
+ * promptfoo has shipped several envelope shapes (the top-level OutputFile with
373
+ * `results.results`, an older `results` list, a bare list); this walks the
374
+ * known shapes rather than pinning one version. */
375
+ export function extractResults(data) {
376
+ if (Array.isArray(data))
377
+ return data.filter(isObject);
378
+ if (!isObject(data))
379
+ return [];
380
+ const results = data.results;
381
+ // New shape: { results: { results: [...] } }
382
+ if (isObject(results) && Array.isArray(results.results)) {
383
+ return results.results.filter(isObject);
384
+ }
385
+ // Older shape: { results: [...] }
386
+ if (Array.isArray(results))
387
+ return results.filter(isObject);
388
+ return [];
389
+ }
390
+ /** Yield `recordOutcome` inputs for every real verdict in a parsed promptfoo
391
+ * output (any envelope shape {@link extractResults} understands). */
392
+ export function iterOutcomes(data, opts) {
393
+ const out = [];
394
+ const results = extractResults(data);
395
+ for (let i = 0; i < results.length; i++) {
396
+ const input = resultToOutcomeInput(results[i], {
397
+ workflowId: opts.workflowId,
398
+ index: i,
399
+ isSimulated: opts.isSimulated,
400
+ onWarn: opts.onWarn,
401
+ });
402
+ if (input !== undefined)
403
+ out.push(input);
404
+ }
405
+ return out;
406
+ }
407
+ /** Emit an outcome per verdict in a parsed promptfoo output through `meter`.
408
+ * Fail-safe: a delivery fault on one row warns and counts an error; it never
409
+ * aborts the batch. Non-verdict rows are counted as skipped. */
410
+ export async function recordResults(meter, data, workflowId, opts = {}) {
411
+ const onWarn = opts.onWarn ?? defaultWarn;
412
+ const results = extractResults(data);
413
+ let recorded = 0;
414
+ let errors = 0;
415
+ let skipped = 0;
416
+ for (let i = 0; i < results.length; i++) {
417
+ const input = resultToOutcomeInput(results[i], {
418
+ workflowId,
419
+ index: i,
420
+ isSimulated: opts.isSimulated,
421
+ onWarn,
422
+ });
423
+ if (input === undefined) {
424
+ skipped++;
425
+ continue;
426
+ }
427
+ try {
428
+ await meter.recordOutcome(input);
429
+ recorded++;
430
+ }
431
+ catch (exc) {
432
+ // Never let a delivery fault abort the batch. (The meter is fail-safe by
433
+ // default and resolves rather than rejects; this guards a strict meter.)
434
+ errors++;
435
+ onWarn(`failed to record outcome for ${input.link}: ${exc.message}`);
436
+ }
437
+ }
438
+ try {
439
+ await meter.flush();
440
+ }
441
+ catch (exc) {
442
+ onWarn(`flush failed: ${exc.message}`);
443
+ }
444
+ return { recorded, errors, skipped };
445
+ }
446
+ //# sourceMappingURL=promptfoo.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"promptfoo.js","sourceRoot":"","sources":["../src/promptfoo.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,190 @@
1
+ /**
2
+ * Read the measured economics of one LLM call straight off a provider response.
3
+ *
4
+ * The TS twin of `margin_meter.providers`: given the object an OpenAI /
5
+ * Anthropic / Gemini JS client returns, recover the `(provider, model, tokens)`
6
+ * a Margin call row needs — so auto-instrumentation is a few lines with no
7
+ * manual token bookkeeping.
8
+ *
9
+ * Dependency-free and duck-typed on purpose: it never imports the provider SDKs,
10
+ * reading fields by shape so it works on a real SDK object, a plain object (the
11
+ * shape a mocked provider returns in tests), or JSON. The three shapes, as the
12
+ * current JS SDKs publish them:
13
+ *
14
+ * - **OpenAI** — `model`; `usage.prompt_tokens` / `completion_tokens`; cached
15
+ * reads at `usage.prompt_tokens_details.cached_tokens` (Responses API:
16
+ * `usage.input_tokens` / `output_tokens` / `input_tokens_details.cached_tokens`).
17
+ * - **Anthropic** — `model`; `usage.input_tokens` / `output_tokens`; cached
18
+ * reads at `usage.cache_read_input_tokens`; cache WRITES at
19
+ * `usage.cache_creation_input_tokens` (a ~1.25x premium bucket).
20
+ * - **Gemini** (`@google/generative-ai`) — `response.usageMetadata` (the
21
+ * `generateContent` result nests it under `.response`), with
22
+ * `promptTokenCount` / `candidatesTokenCount` / `cachedContentTokenCount`, plus
23
+ * `thoughtsTokenCount` (thinking tokens, billed as output and folded in).
24
+ * The model does not ride the response, so the caller passes it.
25
+ * - **Cohere** (v2 `/chat`) — billed counts nest under `usage.billed_units`
26
+ * (what is charged; raw `usage.tokens` is the fallback), each with
27
+ * `input_tokens` / `output_tokens`. Cohere publishes no chat prompt-cache
28
+ * discount, so cache reads bill as ordinary input (`cacheReadTokens` stays 0);
29
+ * the model does not ride the response, so the caller passes it.
30
+ * - **AWS Bedrock** (`@aws-sdk/client-bedrock-runtime`, Converse) — usage rides
31
+ * `usage.inputTokens` / `outputTokens` in camelCase, with ADDITIVE cache
32
+ * buckets `cacheReadInputTokens` / `cacheWriteInputTokens` (AWS documents
33
+ * `totalTokens = inputTokens + outputTokens + cacheReadInputTokens +
34
+ * cacheWriteInputTokens`), so the three input-side buckets are disjoint and
35
+ * each reads verbatim, no subtraction. The Converse response omits the model
36
+ * id, so the caller passes it (`instrumentBedrock` reads it off the command's
37
+ * `input.modelId`); its gateway form (`us.anthropic.claude-…-v1:0`) prices
38
+ * through the server's `normalize_model`.
39
+ *
40
+ * Numbers are read verbatim — never inflated. A field the provider didn't report
41
+ * reads 0; an unrecognised shape throws `UsageExtractionError` rather than
42
+ * emitting a silently-zero (free-looking) call.
43
+ */
44
+ export declare const OPENAI = "openai";
45
+ export declare const ANTHROPIC = "anthropic";
46
+ export declare const GOOGLE = "google";
47
+ export declare const COHERE = "cohere";
48
+ export declare const BEDROCK = "bedrock";
49
+ export type ProviderName = "openai" | "anthropic" | "google" | "cohere" | "bedrock";
50
+ /** The metered economics of one call, recovered from a provider response. */
51
+ export interface Usage {
52
+ provider: ProviderName;
53
+ /** May be undefined only when the provider omits it AND no override was passed. */
54
+ model?: string;
55
+ inputTokens: number;
56
+ outputTokens: number;
57
+ cacheReadTokens: number;
58
+ /**
59
+ * Cache-CREATION (write) tokens — a premium bucket (Anthropic bills ~1.25x
60
+ * input to write a prompt-cache entry). 0 for providers that report no
61
+ * distinct write bucket (OpenAI/Gemini bill the writing call as ordinary
62
+ * input). Kept separate so the server never drops the premium. When a request
63
+ * pins a 1-hour TTL this holds ONLY the 5-minute portion; the 1-hour portion
64
+ * moves to `cacheCreation1hTokens` below.
65
+ */
66
+ cacheCreationTokens: number;
67
+ /**
68
+ * The portion of cache creation written with the LONGER 1-hour TTL, which
69
+ * Anthropic bills at a higher premium (~2x input vs the 5-minute ~1.25x).
70
+ * Disjoint from `cacheCreationTokens`. Optional/0 unless the provider reports a
71
+ * 1-hour breakdown, so a call using only the default cache is unchanged.
72
+ */
73
+ cacheCreation1hTokens?: number;
74
+ /**
75
+ * The hidden-thinking share of `outputTokens` (Gemini `thoughtsTokenCount`,
76
+ * OpenAI `completion_tokens_details.reasoning_tokens`). A SUBSET of output, not
77
+ * an addition — already priced at the output rate, so it is NEVER priced
78
+ * separately (that would double-bill). Carried only for visibility: the
79
+ * thinking-token share is what isolates the reasoning-effort lever from
80
+ * ordinary verbose output. Optional/0 on a call with no reasoning breakdown.
81
+ */
82
+ reasoningTokens?: number;
83
+ /**
84
+ * The provider's OWN reason the generation ended, read VERBATIM — Anthropic
85
+ * `stop_reason` (`end_turn` / `max_tokens` / `tool_use` / `refusal`), OpenAI &
86
+ * Gemini `finish_reason`, Cohere `finish_reason`, Bedrock `stopReason`. A raw
87
+ * SIGNAL, never a billing verdict: a blocked-and-fell-back call (Fable 5's safety
88
+ * fallback) is often NOT billed, but whether it was depends on the customer's
89
+ * opt-in config, so the stop reason alone can never be turned into "this was
90
+ * free" — that is settled by the invoice (MAR-209). Captured only so a downstream
91
+ * reconcile CAN ask the question; `undefined` is an honest UNKNOWN, first-class
92
+ * and never defaulted to a value.
93
+ */
94
+ stopReason?: string;
95
+ /**
96
+ * OpenRouter's own billed cost (`usage.cost`) for a FAN-OUT slug
97
+ * (`openrouter/fusion`), which runs N panel calls plus an analyst call per request,
98
+ * so no token rate prices it (MAR-1309). `undefined` for every other call — the
99
+ * server then prices tokens off its table exactly as before.
100
+ */
101
+ billedCostUsd?: number;
102
+ }
103
+ /** The response object matched no provider shape we can meter. */
104
+ export declare class UsageExtractionError extends Error {
105
+ constructor(message: string);
106
+ }
107
+ type Any = Record<string, unknown> | null | undefined | unknown;
108
+ /**
109
+ * Best-effort provider inference from a response's shape. Prefers unambiguous
110
+ * signals: Gemini exposes `usageMetadata`; Anthropic's usage carries
111
+ * `input_tokens` while OpenAI's carries `prompt_tokens`. Returns undefined when
112
+ * nothing distinguishes it — the caller must then name the provider (the
113
+ * instrumenter always does).
114
+ */
115
+ export declare function detectProvider(response: Any): ProviderName | undefined;
116
+ /** True for `openrouter/fusion` and its siblings (many billed calls per request). */
117
+ export declare function isFanoutSlug(model?: string): boolean;
118
+ /**
119
+ * Recover the metered `Usage` of one call from its provider response. `provider`
120
+ * forces the shape (the instrumenter always knows which client it patched);
121
+ * omit it and the provider is inferred from the response's shape. `model`
122
+ * overrides a response that omits its model (Gemini). Throws
123
+ * `UsageExtractionError` when the shape matches no known provider or carries no
124
+ * usage block, so a silently-free call is never emitted.
125
+ */
126
+ export declare function extractUsage(response: Any, opts?: {
127
+ provider?: ProviderName;
128
+ model?: string;
129
+ }): Usage;
130
+ /**
131
+ * Read whatever usage ONE streaming chunk/event carries, or `undefined`.
132
+ *
133
+ * The three streamed shapes differ in WHERE (and whether) a chunk carries usage:
134
+ *
135
+ * - **OpenAI** (needs `stream_options: { include_usage: true }`): every content
136
+ * chunk has `usage: null`; only the FINAL chunk carries the full `.usage`.
137
+ * - **Gemini**: every chunk carries a CUMULATIVE `usageMetadata` — the last is
138
+ * authoritative, and the running max keeps it.
139
+ * - **Anthropic**: no single event is complete — `message_start` carries the
140
+ * input + cache counts nested under `event.message.usage`, while `message_delta`
141
+ * carries the final `event.usage.output_tokens`. This reads BOTH carriers so the
142
+ * merge can reunite them.
143
+ *
144
+ * Returns `undefined` (never throws) for a chunk with no usage, so an ordinary
145
+ * content chunk is skipped, not treated as an error.
146
+ */
147
+ export declare function usageFromStreamChunk(chunk: Any, provider?: ProviderName, model?: string): Usage | undefined;
148
+ /**
149
+ * Merge two per-chunk usages, taking the MAX of each token bucket.
150
+ *
151
+ * Every streamed token count is monotonic within one call — cumulative (Gemini),
152
+ * or populated exactly once (OpenAI's final chunk; Anthropic's input-here /
153
+ * output-there split) — so the running max reconstructs the final total no matter
154
+ * which chunk carried which field. The max never invents a token; it only keeps
155
+ * the largest REAL count each field reached.
156
+ */
157
+ export declare function mergeStreamUsage(a: Usage, b: Usage): Usage;
158
+ /**
159
+ * The stop reason one stream chunk carries, if any. Never throws.
160
+ *
161
+ * A stream rarely puts the reason and the usage on the same chunk (MAR-1516). OpenAI's
162
+ * finish chunk has no `usage` and the usage chunk after it has empty `choices`; Bedrock
163
+ * `converseStream` sends `stopReason` in `messageStop` and usage in a separate
164
+ * `metadata` event. A reader that only looks at usage-bearing chunks records every
165
+ * streamed call with no reason, so this reads it from whichever chunk holds it.
166
+ */
167
+ export declare function streamChunkStopReason(chunk: Any): string | undefined;
168
+ /**
169
+ * Accumulates the metered usage of one STREAMING call across its chunks.
170
+ *
171
+ * Feed every chunk to `observe` as the host iterates the stream, then call
172
+ * `finalize` once the stream is exhausted (or closed) to get the single `Usage`
173
+ * for the whole call — or `undefined` when the stream never carried a usage block
174
+ * (e.g. an OpenAI stream opened without `include_usage`, where there is genuinely
175
+ * no count to meter and inventing one would be a lie). Provider-agnostic: the
176
+ * per-chunk read + max-merge handles OpenAI, Anthropic and Gemini streams alike.
177
+ */
178
+ export declare class StreamUsageAccumulator {
179
+ private provider?;
180
+ private model?;
181
+ private usage;
182
+ private stopReason;
183
+ constructor(provider?: ProviderName, model?: string);
184
+ /** Fold one chunk's usage (if any) into the running total. Never throws. */
185
+ observe(chunk: Any): void;
186
+ /** The accumulated usage, or `undefined` if the stream carried no usage. */
187
+ finalize(): Usage | undefined;
188
+ }
189
+ export {};
190
+ //# sourceMappingURL=providers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"providers.d.ts","sourceRoot":"","sources":["../src/providers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AAEH,eAAO,MAAM,MAAM,WAAW,CAAC;AAC/B,eAAO,MAAM,SAAS,cAAc,CAAC;AACrC,eAAO,MAAM,MAAM,WAAW,CAAC;AAC/B,eAAO,MAAM,MAAM,WAAW,CAAC;AAC/B,eAAO,MAAM,OAAO,YAAY,CAAC;AAEjC,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,WAAW,GAAG,QAAQ,GAAG,QAAQ,GAAG,SAAS,CAAC;AAEpF,6EAA6E;AAC7E,MAAM,WAAW,KAAK;IACpB,QAAQ,EAAE,YAAY,CAAC;IACvB,mFAAmF;IACnF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,CAAC;IACxB;;;;;;;OAOG;IACH,mBAAmB,EAAE,MAAM,CAAC;IAC5B;;;;;OAKG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B;;;;;;;OAOG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;;;;;;OAUG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,kEAAkE;AAClE,qBAAa,oBAAqB,SAAQ,KAAK;gBACjC,OAAO,EAAE,MAAM;CAI5B;AAED,KAAK,GAAG,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAAC;AAsDhE;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,GAAG,GAAG,YAAY,GAAG,SAAS,CAuBtE;AAoBD,qFAAqF;AACrF,wBAAgB,YAAY,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAGpD;AAiND;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAC1B,QAAQ,EAAE,GAAG,EACb,IAAI,GAAE;IAAE,QAAQ,CAAC,EAAE,YAAY,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAO,GACrD,KAAK,CAUP;AAeD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,GAAG,EACV,QAAQ,CAAC,EAAE,YAAY,EACvB,KAAK,CAAC,EAAE,MAAM,GACb,KAAK,GAAG,SAAS,CAoBnB;AA+ED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,GAAG,KAAK,CAqB1D;AAED;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,GAAG,GAAG,MAAM,GAAG,SAAS,CAqBpE;AAED;;;;;;;;;GASG;AACH,qBAAa,sBAAsB;IACjC,OAAO,CAAC,QAAQ,CAAC,CAAe;IAChC,OAAO,CAAC,KAAK,CAAC,CAAS;IACvB,OAAO,CAAC,KAAK,CAAgC;IAC7C,OAAO,CAAC,UAAU,CAAiC;gBAIvC,QAAQ,CAAC,EAAE,YAAY,EAAE,KAAK,CAAC,EAAE,MAAM;IAKnD,4EAA4E;IAC5E,OAAO,CAAC,KAAK,EAAE,GAAG,GAAG,IAAI;IAYzB,4EAA4E;IAC5E,QAAQ,IAAI,KAAK,GAAG,SAAS;CAK9B"}