margin-meter 0.1.0 → 0.2.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 CHANGED
@@ -15,17 +15,24 @@ authenticated with a per-project ingest key.
15
15
 
16
16
  ## Install
17
17
 
18
- Published as a git-installable subpath of the Margin repo (no npm registry
19
- account needed pre-launch):
18
+ From the npm registry:
20
19
 
21
20
  ```bash
22
21
  npm install margin-meter
23
- # or, pinning the subdirectory with your package manager of choice, e.g. pnpm:
24
- # pnpm add "https://gitpkg.now.sh/subhsubh24/Margin.ai/sdk/ts?main"
25
22
  ```
26
23
 
27
- Most TS projects import the source directly (their bundler compiles it); the
28
- package `exports` point at `src/index.ts` with types.
24
+ > **Check the version you get.** This package ships from the registry, and the
25
+ > registry can lag the repo. Confirm with `npm ls margin-meter`, and compare it
26
+ > against `sdk/ts/package.json` on `main`. An older release silently omits newer
27
+ > capture (e.g. `substrate`, reasoning tokens), which under-reports your spend
28
+ > rather than failing loudly — the one failure mode this SDK exists to prevent.
29
+
30
+ Unlike the [Python SDK](../python), this package is **not** git-installable:
31
+ npm has no equivalent of pip's `#subdirectory=`, `dist/` is a build artifact
32
+ that is not committed, and the entry points resolve to `./dist`. Installing
33
+ from a clone or a git URL therefore yields a package with no entry point. Use
34
+ the registry, or publish a new version from `sdk/ts` (`npm publish` runs the
35
+ build via `prepublishOnly`).
29
36
 
30
37
  ## Configure
31
38
 
@@ -79,6 +86,51 @@ const res = await meter.recordCall({
79
86
  if (!res.ok) console.warn(`margin ingest failed: ${res.error} (${res.statusCode})`);
80
87
  ```
81
88
 
89
+ ## Auto-instrument (a few lines for a whole repo)
90
+
91
+ To instrument an existing agent you didn't write, don't edit every call site.
92
+ Point the meter at the client the agent already constructed and every model call
93
+ is timed and metered automatically — tokens are read straight off each
94
+ provider's response, so there's no `setTokens` bookkeeping:
95
+
96
+ ```ts
97
+ import OpenAI from "openai";
98
+ import { MarginMeter, instrumentOpenAI } from "margin-meter";
99
+
100
+ const client = new OpenAI();
101
+ const meter = new MarginMeter();
102
+ const undo = instrumentOpenAI(client, meter, { workflowId: "aider-fix" });
103
+
104
+ // ... the agent's normal client.chat.completions.create(...) calls are metered ...
105
+
106
+ await meter.recordOutcome({ workflowId: "aider-fix", passed: testsGreen });
107
+ undo(); // restore the original method
108
+ ```
109
+
110
+ `instrumentAnthropic(client, …)` patches `messages.create`;
111
+ `instrumentGemini(model, …)` patches a `GenerativeModel`'s `generateContent`
112
+ (recovering the model id off the instance). Each returns an `undo()` and throws
113
+ `MarginInstrumentError` if the client shape has moved.
114
+
115
+ Two lower-level primitives when you want tighter control:
116
+
117
+ ```ts
118
+ // Wrap ONE call (no monkeypatch) — returns a drop-in replacement.
119
+ const create = meter.wrap(client.chat.completions.create.bind(client.chat.completions), {
120
+ workflowId: "cart-parse",
121
+ provider: "openai",
122
+ });
123
+ const resp = await create({ model: "gpt-4o-mini", messages: [/* … */] }); // metered; resp untouched
124
+
125
+ // Or meter a response you already have.
126
+ await meter.recordResponse({ workflowId: "cart-parse", response: resp, provider: "openai" });
127
+ ```
128
+
129
+ Honesty: tokens are read verbatim from the real response (`extractUsage`) —
130
+ cache reads are billed separately, never double-counted as fresh input — and a
131
+ metering failure is fail-safe (your call still returns / still throws). A failed
132
+ provider call is still recorded with `status:"error"` so it appears in the chain.
133
+
82
134
  ## API
83
135
 
84
136
  | Method | Emits to | Notes |
@@ -86,6 +138,9 @@ if (!res.ok) console.warn(`margin ingest failed: ${res.error} (${res.statusCode}
86
138
  | `recordCall(input)` | `POST /api/ingest/calls` | cost computed from the pricing table when `costUsd` omitted |
87
139
  | `recordOutcome(input)` | `POST /api/ingest/outcomes` | `qualityMethod` records HOW the score was graded |
88
140
  | `measure(input, fn)` | `recordCall` on completion | times latency; `status:"error"` + rethrow if `fn` throws; returns `fn`'s value, emit result on `m.result` |
141
+ | `recordResponse(input)` | `recordCall` | meters straight off a provider response — no manual tokens |
142
+ | `wrap(fn, opts)` | `recordCall` per call | returns an auto-metered drop-in for any LLM call |
143
+ | `instrumentOpenAI` / `instrumentAnthropic` / `instrumentGemini` | — | monkeypatch a client instance's create method |
89
144
 
90
145
  Every method resolves to an `IngestResult { ok, statusCode, body?, error? }`.
91
146
  `ok` is true only on HTTP 200; `body` holds `call_id`/`outcome_id` + `source`.
package/dist/index.d.ts CHANGED
@@ -21,6 +21,12 @@
21
21
  * written `source` is forced server-side to the key's project slug — the
22
22
  * client cannot spoof another project's economics.
23
23
  */
24
+ import { type ProviderName } from "./providers.ts";
25
+ import { type Substrate } from "./substrate.ts";
26
+ export * from "./providers.ts";
27
+ export * from "./instrument.ts";
28
+ export * as substrate from "./substrate.ts";
29
+ export type { Substrate } from "./substrate.ts";
24
30
  export declare const CALLS_PATH = "/api/ingest/calls";
25
31
  export declare const OUTCOMES_PATH = "/api/ingest/outcomes";
26
32
  export declare const KEY_HEADER = "X-Margin-Key";
@@ -84,13 +90,62 @@ export interface RecordCallInput {
84
90
  inputTokens?: number;
85
91
  outputTokens?: number;
86
92
  cacheReadTokens?: number;
93
+ cacheCreationTokens?: number;
94
+ /** The 1-hour-TTL portion of cache creation (Anthropic bills it ~2x input, not
95
+ * the 5-minute ~1.25x). Disjoint from cacheCreationTokens; omit/0 when a call
96
+ * uses only the default cache. */
97
+ cacheCreation1hTokens?: number;
98
+ /** The hidden-thinking share of outputTokens (Gemini thoughts, OpenAI
99
+ * reasoning_tokens). A SUBSET of output, already priced at the output rate —
100
+ * never priced separately. Carried only so the reasoning-effort lever can be
101
+ * isolated; omit/0 on a non-reasoning call. */
102
+ reasoningTokens?: number;
87
103
  latencyMs?: number;
88
104
  status?: string;
89
105
  isRetry?: boolean;
90
106
  sessionId?: string;
91
107
  promptId?: string;
108
+ /** The STEP within a multi-operation workflow this call belongs to (a
109
+ * supply-chain node label the Money Map decomposes by, e.g. "extract"). Omit
110
+ * for a single-call workflow. */
111
+ operation?: string;
92
112
  costUsd?: number;
93
113
  isSimulated?: boolean;
114
+ /** Idempotency key baked into the emitted payload. Defaults to a fresh uuid
115
+ * per call, so a redelivery of THIS exact emit (a transport retry) collapses
116
+ * to one row server-side instead of double-counting spend. Distinct calls get
117
+ * distinct ids, so real volume is never deduped away. */
118
+ eventId?: string;
119
+ /** The provider billing tier. "standard" (the default) is a normal synchronous
120
+ * call; "batch" is a call routed through the async Batch API, which the server
121
+ * prices at the ~50%-off tier — pricing a real batch call standard would
122
+ * over-state its cost. Omit for standard; the field is sent only when "batch". */
123
+ billingTier?: "standard" | "batch";
124
+ /** This call's own node in an agent tree. A branching run is otherwise a flat
125
+ * list of calls sharing a sessionId — enough to total the run, never enough to
126
+ * ask what one BRANCH cost. Omit for a single-agent call. */
127
+ spanId?: string;
128
+ /** The node that spawned this one; omit at the root. Requires `spanId` (a parent
129
+ * link with no node of its own is an edge to nowhere, and the server rejects
130
+ * it). */
131
+ parentSpanId?: string;
132
+ /** The orchestration shape THIS call performed, on the node that did the
133
+ * orchestrating — not on its branches. The set is closed server-side so shapes
134
+ * stay comparable across runs. */
135
+ orchestrationPattern?: "pipeline" | "barrier" | "panel" | "loop_until_dry";
136
+ /** How many branches the orchestration opened. */
137
+ branchWidth?: number;
138
+ /** The rule that ended the orchestration, free text (e.g. "majority of 3"). */
139
+ stoppingRule?: string;
140
+ /** Which PATH this call took to reach the model — the vendor's own API
141
+ * ("direct"), a router ("openrouter", "litellm"), a cloud reseller ("azure",
142
+ * "bedrock", "vertex"), or an unrecognised proxy ("gateway"). Independent of
143
+ * `provider`, which says who owns the model and bills for it: the same
144
+ * openai/gpt-4o call can arrive over four different paths, and comparing them
145
+ * is something a single-substrate router structurally cannot do. Omit it when
146
+ * the path is not actually known — an unmeasured dimension belongs empty, not
147
+ * defaulted to "direct". The set is closed server-side. */
148
+ substrate?: "direct" | "openrouter" | "litellm" | "azure" | "bedrock" | "vertex" | "gateway";
94
149
  }
95
150
  export interface RecordOutcomeInput {
96
151
  workflowId: string;
@@ -99,6 +154,9 @@ export interface RecordOutcomeInput {
99
154
  qualityMethod?: string;
100
155
  link?: string;
101
156
  isSimulated?: boolean;
157
+ /** Idempotency key (see RecordCallInput.eventId): a redelivered outcome does
158
+ * not double-count the cost-per-outcome denominator. Defaults to a fresh uuid. */
159
+ eventId?: string;
102
160
  }
103
161
  /** A per-project client that emits measured economics to a Margin ingest API. */
104
162
  export declare class MarginMeter {
@@ -122,17 +180,83 @@ export declare class MarginMeter {
122
180
  * value `fn` produced; the emit's `IngestResult` is available as `m.result`.
123
181
  */
124
182
  measure<T>(input: Omit<RecordCallInput, "latencyMs" | "status">, fn: (m: Measurement) => Promise<T> | T): Promise<T>;
183
+ /**
184
+ * Meter one call by reading tokens straight off its provider `response`
185
+ * (OpenAI / Anthropic / Gemini result). The no-bookkeeping counterpart to
186
+ * `recordCall`: the `(provider, model, tokens)` are recovered by
187
+ * `extractUsage`. `provider` is optional when the shape is unambiguous but the
188
+ * instrumenter always passes it; `model` overrides a response that omits it
189
+ * (Gemini). Numbers are read verbatim. Never throws unless `raiseOnError`.
190
+ */
191
+ recordResponse(input: RecordResponseInput): Promise<IngestResult>;
192
+ /**
193
+ * Return `fn` wrapped so every call is timed and auto-metered. The core
194
+ * auto-instrumentation primitive: give it any async LLM call
195
+ * (`client.chat.completions.create`, `client.messages.create`,
196
+ * `model.generateContent` …) and it returns a drop-in replacement that times
197
+ * the call, emits its economics from the returned response, and hands the
198
+ * ORIGINAL response back untouched. On a throw the call is metered with
199
+ * `status="error"` and the error re-thrown (never swallowed); a metering
200
+ * failure is fail-safe and never breaks the wrapped call. `provider` is
201
+ * required for a shape the extractor can't disambiguate; `model` supplies a
202
+ * model the response omits (Gemini); `resolveModel(args)` derives it per call
203
+ * (e.g. off a bound Gemini model). Signature-agnostic (`...args`).
204
+ */
205
+ wrap<F extends (...args: never[]) => Promise<unknown>>(fn: F, opts: WrapOptions): (...args: Parameters<F>) => Promise<Awaited<ReturnType<F>>>;
206
+ }
207
+ /** Input to `recordResponse` — meter off a provider response object. */
208
+ export interface RecordResponseInput {
209
+ workflowId: string;
210
+ response: unknown;
211
+ provider?: ProviderName;
212
+ model?: string;
213
+ latencyMs?: number;
214
+ status?: string;
215
+ isRetry?: boolean;
216
+ sessionId?: string;
217
+ promptId?: string;
218
+ operation?: string;
219
+ isSimulated?: boolean;
220
+ /** The delivery path the call took (see `substrate.ts`); omit when unknown. */
221
+ substrate?: Substrate;
222
+ }
223
+ /** Options for `wrap` — how to meter every call to the wrapped fn. */
224
+ export interface WrapOptions {
225
+ workflowId: string;
226
+ provider?: ProviderName;
227
+ model?: string;
228
+ operation?: string;
229
+ sessionId?: string;
230
+ isRetry?: boolean;
231
+ isSimulated?: boolean;
232
+ /** Derive the model per call (e.g. off a bound Gemini model); a truthy return
233
+ * overrides the static `model`. Receives the wrapped call's args. */
234
+ resolveModel?: (args: unknown[]) => string | undefined;
235
+ /** OpenAI omits usage from a stream unless asked; when set (only by the OpenAI
236
+ * chat instrumenter), a `stream: true` call gets `stream_options.include_usage`
237
+ * injected so its final chunk carries the count the accumulator meters. */
238
+ streamIncludeUsage?: boolean;
239
+ /** Which DELIVERY PATH this wrapper's calls take (see `substrate.ts`). The JS
240
+ * instrumenters patch a specific client instance, so the endpoint is known at
241
+ * patch time and every call through this wrapper travelled it. Leave unset when
242
+ * the endpoint could not be read — an unmeasured dimension belongs empty, never
243
+ * defaulted to "direct". Never touches the cost math. */
244
+ substrate?: Substrate;
125
245
  }
126
246
  /** Token accumulator passed into `measure`'s callback. */
127
247
  export declare class Measurement {
128
248
  inputTokens: number;
129
249
  outputTokens: number;
130
250
  cacheReadTokens: number;
251
+ cacheCreationTokens: number;
252
+ cacheCreation1hTokens: number;
131
253
  result?: IngestResult;
132
254
  setTokens(tokens: {
133
255
  inputTokens?: number;
134
256
  outputTokens?: number;
135
257
  cacheReadTokens?: number;
258
+ cacheCreationTokens?: number;
259
+ cacheCreation1hTokens?: number;
136
260
  }): void;
137
261
  }
138
262
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -21,11 +21,30 @@
21
21
  * written `source` is forced server-side to the key's project slug — the
22
22
  * client cannot spoof another project's economics.
23
23
  */
24
+ import { extractUsage, StreamUsageAccumulator, } from "./providers.js";
25
+ export * from "./providers.js";
26
+ export * from "./instrument.js";
27
+ // Namespaced, not flattened: `BEDROCK` means two different things here — the
28
+ // provider that bills for the model and the path the request took — and merging
29
+ // them into one barrel would collide the names and blur the distinction the
30
+ // substrate dimension exists to draw. `substrate.BEDROCK` reads as the path.
31
+ export * as substrate from "./substrate.js";
24
32
  export const CALLS_PATH = "/api/ingest/calls";
25
33
  export const OUTCOMES_PATH = "/api/ingest/outcomes";
26
34
  export const KEY_HEADER = "X-Margin-Key";
27
35
  export const DEFAULT_INGEST_URL = "http://127.0.0.1:8000";
28
36
  export const DEFAULT_TIMEOUT_MS = 10_000;
37
+ /** A fresh idempotency key per emit. Uses the platform crypto UUID (Node 16.7+,
38
+ * modern browsers); falls back to a random-hex id where crypto is absent, which
39
+ * is still unique enough to key a single call's dedup. */
40
+ function newEventId() {
41
+ const c = globalThis.crypto;
42
+ if (c?.randomUUID)
43
+ return c.randomUUID();
44
+ return (Date.now().toString(16) +
45
+ Math.random().toString(16).slice(2) +
46
+ Math.random().toString(16).slice(2));
47
+ }
29
48
  /** Raised (only in strict mode) when a row is rejected by the ingest API. */
30
49
  export class MarginIngestError extends Error {
31
50
  statusCode;
@@ -115,17 +134,57 @@ export class MarginMeter {
115
134
  input_tokens: input.inputTokens ?? 0,
116
135
  output_tokens: input.outputTokens ?? 0,
117
136
  cache_read_tokens: input.cacheReadTokens ?? 0,
137
+ cache_creation_tokens: input.cacheCreationTokens ?? 0,
118
138
  latency_ms: input.latencyMs ?? 0,
119
139
  status: input.status ?? "ok",
120
140
  is_retry: input.isRetry ?? false,
121
141
  is_simulated: input.isSimulated ?? false,
142
+ event_id: input.eventId ?? newEventId(),
122
143
  };
123
144
  if (input.sessionId !== undefined)
124
145
  payload.session_id = input.sessionId;
125
146
  if (input.promptId !== undefined)
126
147
  payload.prompt_id = input.promptId;
148
+ if (input.operation !== undefined)
149
+ payload.operation = input.operation;
127
150
  if (input.costUsd !== undefined)
128
151
  payload.cost_usd = input.costUsd;
152
+ // Only send a non-default tier: a synchronous call omits it (the server
153
+ // defaults "standard"), keeping the wire minimal and an older server that
154
+ // predates the field unaffected.
155
+ if (input.billingTier !== undefined && input.billingTier !== "standard") {
156
+ payload.billing_tier = input.billingTier;
157
+ }
158
+ // Only send the 1-hour cache-write bucket when a call used one: the common
159
+ // single-TTL call omits it (the server reads 0 and prices its whole cache
160
+ // creation at the 5-minute rate, unchanged), keeping the wire minimal and an
161
+ // older server that predates the field unaffected.
162
+ if (input.cacheCreation1hTokens) {
163
+ payload.cache_creation_1h_tokens = input.cacheCreation1hTokens;
164
+ }
165
+ // Only send the reasoning subset when a call spent thinking tokens: a
166
+ // non-reasoning call omits it (server reads 0). It never changes cost — it is a
167
+ // share of output already priced at the output rate.
168
+ if (input.reasoningTokens) {
169
+ payload.reasoning_tokens = input.reasoningTokens;
170
+ }
171
+ // The agent-tree fields ride only when a call belongs to one, so a
172
+ // single-agent call's wire is unchanged and an older server that predates them
173
+ // is unaffected. They carry structure, never money — no cost branch reads them.
174
+ if (input.spanId !== undefined)
175
+ payload.span_id = input.spanId;
176
+ if (input.parentSpanId !== undefined)
177
+ payload.parent_span_id = input.parentSpanId;
178
+ if (input.orchestrationPattern !== undefined) {
179
+ payload.orchestration_pattern = input.orchestrationPattern;
180
+ }
181
+ if (input.branchWidth !== undefined)
182
+ payload.branch_width = input.branchWidth;
183
+ if (input.stoppingRule !== undefined)
184
+ payload.stopping_rule = input.stoppingRule;
185
+ // The delivery path, sent only when it was actually determined.
186
+ if (input.substrate !== undefined)
187
+ payload.substrate = input.substrate;
129
188
  return await this.emit(CALLS_PATH, payload);
130
189
  }
131
190
  /** Emit one outcome (a unit of productivity). `qualityMethod` records HOW the
@@ -135,6 +194,7 @@ export class MarginMeter {
135
194
  workflow_id: input.workflowId,
136
195
  passed: input.passed,
137
196
  is_simulated: input.isSimulated ?? false,
197
+ event_id: input.eventId ?? newEventId(),
138
198
  };
139
199
  if (input.qualityScore !== undefined)
140
200
  payload.quality_score = input.qualityScore;
@@ -169,22 +229,275 @@ export class MarginMeter {
169
229
  inputTokens: m.inputTokens,
170
230
  outputTokens: m.outputTokens,
171
231
  cacheReadTokens: m.cacheReadTokens,
232
+ cacheCreationTokens: m.cacheCreationTokens,
233
+ cacheCreation1hTokens: m.cacheCreation1hTokens,
172
234
  latencyMs,
173
235
  status: threw ? "error" : "ok",
174
236
  });
175
237
  }
176
238
  }
239
+ // --------------------------------------------------------------------- //
240
+ // Auto-instrumentation — meter straight off a provider response, no manual
241
+ // setTokens(). The "few lines to instrument a repo" path.
242
+ // --------------------------------------------------------------------- //
243
+ /**
244
+ * Meter one call by reading tokens straight off its provider `response`
245
+ * (OpenAI / Anthropic / Gemini result). The no-bookkeeping counterpart to
246
+ * `recordCall`: the `(provider, model, tokens)` are recovered by
247
+ * `extractUsage`. `provider` is optional when the shape is unambiguous but the
248
+ * instrumenter always passes it; `model` overrides a response that omits it
249
+ * (Gemini). Numbers are read verbatim. Never throws unless `raiseOnError`.
250
+ */
251
+ async recordResponse(input) {
252
+ const usage = extractUsage(input.response, {
253
+ provider: input.provider,
254
+ model: input.model,
255
+ });
256
+ return await this.recordCall({
257
+ workflowId: input.workflowId,
258
+ provider: usage.provider,
259
+ model: usage.model ?? "",
260
+ inputTokens: usage.inputTokens,
261
+ outputTokens: usage.outputTokens,
262
+ cacheReadTokens: usage.cacheReadTokens,
263
+ cacheCreationTokens: usage.cacheCreationTokens,
264
+ cacheCreation1hTokens: usage.cacheCreation1hTokens,
265
+ reasoningTokens: usage.reasoningTokens,
266
+ latencyMs: input.latencyMs,
267
+ status: input.status ?? "ok",
268
+ isRetry: input.isRetry,
269
+ sessionId: input.sessionId,
270
+ promptId: input.promptId,
271
+ operation: input.operation,
272
+ isSimulated: input.isSimulated,
273
+ substrate: input.substrate,
274
+ });
275
+ }
276
+ /**
277
+ * Return `fn` wrapped so every call is timed and auto-metered. The core
278
+ * auto-instrumentation primitive: give it any async LLM call
279
+ * (`client.chat.completions.create`, `client.messages.create`,
280
+ * `model.generateContent` …) and it returns a drop-in replacement that times
281
+ * the call, emits its economics from the returned response, and hands the
282
+ * ORIGINAL response back untouched. On a throw the call is metered with
283
+ * `status="error"` and the error re-thrown (never swallowed); a metering
284
+ * failure is fail-safe and never breaks the wrapped call. `provider` is
285
+ * required for a shape the extractor can't disambiguate; `model` supplies a
286
+ * model the response omits (Gemini); `resolveModel(args)` derives it per call
287
+ * (e.g. off a bound Gemini model). Signature-agnostic (`...args`).
288
+ */
289
+ wrap(fn, opts) {
290
+ const meter = this;
291
+ return async function (...args) {
292
+ let callModel = opts.model;
293
+ if (opts.resolveModel) {
294
+ try {
295
+ const resolved = opts.resolveModel(args);
296
+ if (resolved)
297
+ callModel = resolved;
298
+ }
299
+ catch {
300
+ /* a resolver hiccup never breaks the wrapped call */
301
+ }
302
+ }
303
+ // Ask OpenAI to include usage on a streamed call so the final chunk carries
304
+ // the count the accumulator needs (only when this is an OpenAI chat patch).
305
+ maybeInjectIncludeUsage(args, opts);
306
+ const start = now();
307
+ let response;
308
+ try {
309
+ response = (await fn(...args));
310
+ }
311
+ catch (e) {
312
+ const latencyMs = Math.round(now() - start);
313
+ try {
314
+ await meter.recordCall({
315
+ workflowId: opts.workflowId,
316
+ provider: opts.provider ?? "unknown",
317
+ model: callModel ?? "",
318
+ latencyMs,
319
+ status: "error",
320
+ isRetry: opts.isRetry,
321
+ sessionId: opts.sessionId,
322
+ operation: opts.operation,
323
+ isSimulated: opts.isSimulated,
324
+ substrate: opts.substrate,
325
+ });
326
+ }
327
+ catch {
328
+ /* metering must never mask the host's real exception */
329
+ }
330
+ throw e;
331
+ }
332
+ // A streamed call resolves to an async-iterable of chunks, not a `.usage`
333
+ // response. Reading it here would throw and drop the call; instead hand back
334
+ // a transparent proxy that meters ONCE as the host consumes the stream.
335
+ if (isAsyncIterable(response)) {
336
+ return meteredStream(response, meter, opts, callModel, start);
337
+ }
338
+ const latencyMs = Math.round(now() - start);
339
+ try {
340
+ await meter.recordResponse({
341
+ workflowId: opts.workflowId,
342
+ response,
343
+ provider: opts.provider,
344
+ model: callModel,
345
+ latencyMs,
346
+ isRetry: opts.isRetry,
347
+ sessionId: opts.sessionId,
348
+ operation: opts.operation,
349
+ isSimulated: opts.isSimulated,
350
+ substrate: opts.substrate,
351
+ });
352
+ }
353
+ catch {
354
+ /* fail-safe: an unreadable response must not break the host call */
355
+ }
356
+ return response;
357
+ };
358
+ }
359
+ }
360
+ /** True when `x` is an async-iterable (a provider stream), never a plain response
361
+ * object, string, or array. The clean discriminator between a streamed call and
362
+ * a normal one after the wrapped fn resolves. */
363
+ function isAsyncIterable(x) {
364
+ return (x != null &&
365
+ (typeof x === "object" || typeof x === "function") &&
366
+ typeof x[Symbol.asyncIterator] === "function");
367
+ }
368
+ /** When this wrapper is an OpenAI chat patch (`streamIncludeUsage`) AND the call
369
+ * requests `stream: true`, add `stream_options.include_usage` so OpenAI emits its
370
+ * usage on the final chunk. Clones the params object (never mutates the caller's),
371
+ * and fills only when absent so an explicit caller choice is respected. */
372
+ function maybeInjectIncludeUsage(args, opts) {
373
+ if (!opts.streamIncludeUsage)
374
+ return;
375
+ const params = args[0];
376
+ if (params == null || typeof params !== "object")
377
+ return;
378
+ const p = params;
379
+ if (p.stream !== true)
380
+ return;
381
+ const existing = p.stream_options;
382
+ if (existing == null) {
383
+ args[0] = { ...p, stream_options: { include_usage: true } };
384
+ }
385
+ else if (typeof existing === "object" &&
386
+ !("include_usage" in existing)) {
387
+ args[0] = {
388
+ ...p,
389
+ stream_options: { ...existing, include_usage: true },
390
+ };
391
+ }
392
+ }
393
+ /**
394
+ * A transparent proxy over a provider stream that meters on completion.
395
+ *
396
+ * The TS twin of `margin_meter`'s `_MeteredStream`. It hands every chunk to the
397
+ * host UNCHANGED while feeding it to a `StreamUsageAccumulator`; when the stream
398
+ * is exhausted, the host breaks early (its iterator's `return()`), or a chunk
399
+ * throws, it meters exactly once with the accumulated usage — or nothing when the
400
+ * stream carried no usage at all (an OpenAI stream without `include_usage`), since
401
+ * a zero-token row would misreport a real call as free. Every other property/method
402
+ * delegates to the wrapped stream, so `for await (const c of stream)`, `.tee()`,
403
+ * and the rest work unchanged. Fail-safe: a metering hiccup never breaks iteration.
404
+ */
405
+ function meteredStream(stream, meter, opts, callModel, start) {
406
+ const acc = new StreamUsageAccumulator(opts.provider, callModel);
407
+ let finished = false;
408
+ const finish = async () => {
409
+ if (finished)
410
+ return;
411
+ finished = true;
412
+ const usage = acc.finalize();
413
+ // Honest: a stream that carried no usage is NOT recorded — there is no real
414
+ // token count and a zero-token row would report the call as free.
415
+ if (usage === undefined)
416
+ return;
417
+ const latencyMs = Math.round(now() - start);
418
+ try {
419
+ await meter.recordCall({
420
+ workflowId: opts.workflowId,
421
+ provider: usage.provider ?? opts.provider ?? "unknown",
422
+ model: usage.model ?? callModel ?? "",
423
+ inputTokens: usage.inputTokens,
424
+ outputTokens: usage.outputTokens,
425
+ cacheReadTokens: usage.cacheReadTokens,
426
+ cacheCreationTokens: usage.cacheCreationTokens,
427
+ cacheCreation1hTokens: usage.cacheCreation1hTokens,
428
+ reasoningTokens: usage.reasoningTokens,
429
+ latencyMs,
430
+ status: "ok",
431
+ isRetry: opts.isRetry,
432
+ sessionId: opts.sessionId,
433
+ operation: opts.operation,
434
+ isSimulated: opts.isSimulated,
435
+ substrate: opts.substrate,
436
+ });
437
+ }
438
+ catch {
439
+ /* metering must never mask the host's real result */
440
+ }
441
+ };
442
+ return new Proxy(stream, {
443
+ get(target, prop, receiver) {
444
+ if (prop === Symbol.asyncIterator) {
445
+ return function meteredAsyncIterator() {
446
+ const inner = target[Symbol.asyncIterator]();
447
+ return {
448
+ async next() {
449
+ let r;
450
+ try {
451
+ r = await inner.next();
452
+ }
453
+ catch (e) {
454
+ await finish(); // a mid-stream error still banks the partial usage seen
455
+ throw e;
456
+ }
457
+ if (r.done) {
458
+ await finish(); // metered once, cleanly, at the end of the stream
459
+ }
460
+ else {
461
+ try {
462
+ acc.observe(r.value);
463
+ }
464
+ catch {
465
+ /* a malformed chunk never breaks the host's read */
466
+ }
467
+ }
468
+ return r;
469
+ },
470
+ async return(value) {
471
+ await finish(); // the host broke early — meter what was seen
472
+ if (typeof inner.return === "function") {
473
+ return inner.return(value);
474
+ }
475
+ return { done: true, value };
476
+ },
477
+ };
478
+ };
479
+ }
480
+ const val = Reflect.get(target, prop, receiver);
481
+ return typeof val === "function"
482
+ ? val.bind(target)
483
+ : val;
484
+ },
485
+ });
177
486
  }
178
487
  /** Token accumulator passed into `measure`'s callback. */
179
488
  export class Measurement {
180
489
  inputTokens = 0;
181
490
  outputTokens = 0;
182
491
  cacheReadTokens = 0;
492
+ cacheCreationTokens = 0;
493
+ cacheCreation1hTokens = 0;
183
494
  result;
184
495
  setTokens(tokens) {
185
496
  this.inputTokens = tokens.inputTokens ?? 0;
186
497
  this.outputTokens = tokens.outputTokens ?? 0;
187
498
  this.cacheReadTokens = tokens.cacheReadTokens ?? 0;
499
+ this.cacheCreationTokens = tokens.cacheCreationTokens ?? 0;
500
+ this.cacheCreation1hTokens = tokens.cacheCreation1hTokens ?? 0;
188
501
  }
189
502
  }
190
503
  async function extractDetail(resp) {